From b7ccbca34e505e8f50e706849c4167c953138326 Mon Sep 17 00:00:00 2001
From: Thomas Kosiewski <thoma471@googlemail.com>
Date: Fri, 7 Mar 2025 16:12:20 +0100
Subject: [PATCH 01/25] ci: fix cask description to pass brew doctor lint
 (#102)

Updated description for Coder Desktop in Homebrew cask

Change-Id: Ieed479ea7d73ba4bb86ae3105a870970bfd60e58
Signed-off-by: Thomas Kosiewski <tk@coder.com>
---
 scripts/update-cask.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/update-cask.sh b/scripts/update-cask.sh
index 4524ecfb..c9a71a54 100755
--- a/scripts/update-cask.sh
+++ b/scripts/update-cask.sh
@@ -99,7 +99,7 @@ cask "coder-desktop${SUFFIX}" do
 
   url "https://github.com/coder/coder-desktop-macos/releases/download/$([ "$IS_PREVIEW" = true ] && echo "${TAG}" || echo "v#{version}")/CoderDesktop.pkg"
   name "Coder Desktop"
-  desc "Coder Desktop client"
+  desc "Native desktop client for Coder"
   homepage "https://github.com/coder/coder-desktop-macos"
 
   conflicts_with cask: "coder/coder/${CONFLICTS_WITH}"

From 2094e9fea32d141854524a5b37497d44fe4bea90 Mon Sep 17 00:00:00 2001
From: Thomas Kosiewski <thoma471@googlemail.com>
Date: Tue, 11 Mar 2025 10:44:56 +0100
Subject: [PATCH 02/25] chore: remove "VPN" from setting description (#86)

Rename "CoderVPN" to "Coder Connect" in UI and system components

- Updates VPN-related text from "CoderVPN" to "Coder Connect" in UI components
- Changes network extension references from "VPN" to "NE" in system messages
- Updates VPN tunnel name from "CoderVPN" to "Coder" in network configuration
- Modifies installation scripts to use "Coder" as the VPN service name
---
 Coder Desktop/Coder Desktop/NetworkExtension.swift     |  2 +-
 Coder Desktop/Coder Desktop/SystemExtension.swift      |  8 ++++----
 .../Coder Desktop/Views/Settings/GeneralTab.swift      |  2 +-
 .../Views/Settings/LiteralHeadersSection.swift         |  2 +-
 Coder Desktop/Coder Desktop/Views/VPNMenu.swift        |  2 +-
 Coder Desktop/Coder Desktop/Views/VPNState.swift       |  6 +++---
 Coder Desktop/Coder DesktopTests/VPNMenuTests.swift    |  2 +-
 Coder Desktop/Coder DesktopTests/VPNStateTests.swift   |  6 +++---
 pkgbuild/scripts/postinstall                           |  2 +-
 pkgbuild/scripts/preinstall                            | 10 +++++-----
 10 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/Coder Desktop/Coder Desktop/NetworkExtension.swift b/Coder Desktop/Coder Desktop/NetworkExtension.swift
index c650d163..660ef37d 100644
--- a/Coder Desktop/Coder Desktop/NetworkExtension.swift	
+++ b/Coder Desktop/Coder Desktop/NetworkExtension.swift	
@@ -50,7 +50,7 @@ extension CoderVPNService {
         logger.debug("inserting new tunnel")
 
         let tm = NETunnelProviderManager()
-        tm.localizedDescription = "CoderVPN"
+        tm.localizedDescription = "Coder"
         tm.protocolConfiguration = proto
 
         logger.debug("saving new tunnel")
diff --git a/Coder Desktop/Coder Desktop/SystemExtension.swift b/Coder Desktop/Coder Desktop/SystemExtension.swift
index 0ded6dd3..aade55d9 100644
--- a/Coder Desktop/Coder Desktop/SystemExtension.swift	
+++ b/Coder Desktop/Coder Desktop/SystemExtension.swift	
@@ -11,13 +11,13 @@ enum SystemExtensionState: Equatable, Sendable {
     var description: String {
         switch self {
         case .uninstalled:
-            "VPN SystemExtension is waiting to be activated"
+            "NE SystemExtension is waiting to be activated"
         case .needsUserApproval:
-            "VPN SystemExtension needs user approval to activate"
+            "NE SystemExtension needs user approval to activate"
         case .installed:
-            "VPN SystemExtension is installed"
+            "NE SystemExtension is installed"
         case let .failed(error):
-            "VPN SystemExtension failed with error: \(error)"
+            "NE SystemExtension failed with error: \(error)"
         }
     }
 }
diff --git a/Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift b/Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift
index 0417d03b..27aecabb 100644
--- a/Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift	
+++ b/Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift	
@@ -10,7 +10,7 @@ struct GeneralTab: View {
             }
             Section {
                 Toggle(isOn: $state.stopVPNOnQuit) {
-                    Text("Stop VPN on Quit")
+                    Text("Stop Coder Connect on Quit")
                 }
             }
         }.formStyle(.grouped)
diff --git a/Coder Desktop/Coder Desktop/Views/Settings/LiteralHeadersSection.swift b/Coder Desktop/Coder Desktop/Views/Settings/LiteralHeadersSection.swift
index e3a47b9d..e9a9b056 100644
--- a/Coder Desktop/Coder Desktop/Views/Settings/LiteralHeadersSection.swift	
+++ b/Coder Desktop/Coder Desktop/Views/Settings/LiteralHeadersSection.swift	
@@ -15,7 +15,7 @@ struct LiteralHeadersSection<VPN: VPNService>: View {
             Toggle(isOn: $state.useLiteralHeaders) {
                 Text("HTTP Headers")
                 Text("When enabled, these headers will be included on all outgoing HTTP requests.")
-                if vpn.state != .disabled { Text("Cannot be modified while Coder VPN is enabled.") }
+                if vpn.state != .disabled { Text("Cannot be modified while Coder Connect is enabled.") }
             }
             .controlSize(.large)
 
diff --git a/Coder Desktop/Coder Desktop/Views/VPNMenu.swift b/Coder Desktop/Coder Desktop/Views/VPNMenu.swift
index fe1f2199..352123de 100644
--- a/Coder Desktop/Coder Desktop/Views/VPNMenu.swift	
+++ b/Coder Desktop/Coder Desktop/Views/VPNMenu.swift	
@@ -20,7 +20,7 @@ struct VPNMenu<VPN: VPNService>: View {
                         }
                         }
                     )) {
-                        Text("CoderVPN")
+                        Text("Coder Connect")
                             .frame(maxWidth: .infinity, alignment: .leading)
                             .font(.body.bold())
                             .foregroundColor(.primary)
diff --git a/Coder Desktop/Coder Desktop/Views/VPNState.swift b/Coder Desktop/Coder Desktop/Views/VPNState.swift
index 8ef4e2b2..64c08568 100644
--- a/Coder Desktop/Coder Desktop/Views/VPNState.swift	
+++ b/Coder Desktop/Coder Desktop/Views/VPNState.swift	
@@ -14,18 +14,18 @@ struct VPNState<VPN: VPNService>: View {
                     .font(.body)
                     .foregroundStyle(.secondary)
             case (_, false):
-                Text("Sign in to use CoderVPN")
+                Text("Sign in to use Coder Desktop")
                     .font(.body)
                     .foregroundColor(.secondary)
             case (.disabled, _):
-                Text("Enable CoderVPN to see workspaces")
+                Text("Enable Coder Connect to see workspaces")
                     .font(.body)
                     .foregroundStyle(.secondary)
             case (.connecting, _), (.disconnecting, _):
                 HStack {
                     Spacer()
                     ProgressView(
-                        vpn.state == .connecting ? "Starting CoderVPN..." : "Stopping CoderVPN..."
+                        vpn.state == .connecting ? "Starting Coder Connect..." : "Stopping Coder Connect..."
                     ).padding()
                     Spacer()
                 }
diff --git a/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift b/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift
index da699abc..c38a062d 100644
--- a/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift	
+++ b/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift	
@@ -24,7 +24,7 @@ struct VPNMenuTests {
             try await sut.inspection.inspect { view in
                 let toggle = try view.find(ViewType.Toggle.self)
                 #expect(toggle.isDisabled())
-                #expect(throws: Never.self) { try view.find(text: "Sign in to use CoderVPN") }
+                #expect(throws: Never.self) { try view.find(text: "Sign in to use Coder Desktop") }
                 #expect(throws: Never.self) { try view.find(button: "Sign in") }
             }
         }
diff --git a/Coder Desktop/Coder DesktopTests/VPNStateTests.swift b/Coder Desktop/Coder DesktopTests/VPNStateTests.swift
index d4affc97..92827cf8 100644
--- a/Coder Desktop/Coder DesktopTests/VPNStateTests.swift	
+++ b/Coder Desktop/Coder DesktopTests/VPNStateTests.swift	
@@ -26,7 +26,7 @@ struct VPNStateTests {
         try await ViewHosting.host(view) {
             try await sut.inspection.inspect { view in
                 #expect(throws: Never.self) {
-                    try view.find(text: "Enable CoderVPN to see workspaces")
+                    try view.find(text: "Enable Coder Connect to see workspaces")
                 }
             }
         }
@@ -39,7 +39,7 @@ struct VPNStateTests {
         try await ViewHosting.host(view) {
             try await sut.inspection.inspect { view in
                 let progressView = try view.find(ViewType.ProgressView.self)
-                #expect(try progressView.labelView().text().string() == "Starting CoderVPN...")
+                #expect(try progressView.labelView().text().string() == "Starting Coder Connect...")
             }
         }
     }
@@ -51,7 +51,7 @@ struct VPNStateTests {
         try await ViewHosting.host(view) {
             try await sut.inspection.inspect { view in
                 let progressView = try view.find(ViewType.ProgressView.self)
-                #expect(try progressView.labelView().text().string() == "Stopping CoderVPN...")
+                #expect(try progressView.labelView().text().string() == "Stopping Coder Connect...")
             }
         }
     }
diff --git a/pkgbuild/scripts/postinstall b/pkgbuild/scripts/postinstall
index 57d129a4..b7dd1bd3 100755
--- a/pkgbuild/scripts/postinstall
+++ b/pkgbuild/scripts/postinstall
@@ -16,7 +16,7 @@ if [ -f "$VPN_MARKER_FILE" ]; then
   echo "Restarting CoderVPN..."
   echo "Sleeping for 3..."
   sleep 3
-  scutil --nc start "CoderVPN"
+  scutil --nc start "Coder"
   rm "$VPN_MARKER_FILE"
   echo "CoderVPN started."
 fi
diff --git a/pkgbuild/scripts/preinstall b/pkgbuild/scripts/preinstall
index a26d6c44..66c54e92 100755
--- a/pkgbuild/scripts/preinstall
+++ b/pkgbuild/scripts/preinstall
@@ -10,19 +10,19 @@ if pgrep 'Coder Desktop'; then
 fi
 
 echo "Turning off VPN"
-if scutil --nc list | grep -q "CoderVPN"; then
+if scutil --nc list | grep -q "Coder"; then
   echo "CoderVPN found. Stopping..."
-  if scutil --nc status "CoderVPN" | grep -q "^Connected$"; then
+  if scutil --nc status "Coder" | grep -q "^Connected$"; then
     touch $VPN_MARKER_FILE
   fi
-  scutil --nc stop "CoderVPN"
+  scutil --nc stop "Coder"
 
   # Wait for VPN to be disconnected
-  while scutil --nc status "CoderVPN" | grep -q "^Connected$"; do
+  while scutil --nc status "Coder" | grep -q "^Connected$"; do
     echo "Waiting for VPN to disconnect..."
     sleep 1
   done
-  while scutil --nc status "CoderVPN" | grep -q "^Disconnecting$"; do
+  while scutil --nc status "Coder" | grep -q "^Disconnecting$"; do
     echo "Waiting for VPN to complete disconnect..."
     sleep 1
   done

From 6f6049e9b2b93aade61bf083241fd37b3f4400ad Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 12 Mar 2025 16:43:13 +1100
Subject: [PATCH 03/25] chore: manage mutagen daemon lifecycle (#98)

Closes https://github.com/coder/internal/issues/381.

- Moves the VPN-specific app files into a `VPN` folder.
- Adds an empty `Resources` folder whose contents are copied into the bundle at build time.
- Adds a `MutagenDaemon` abstraction for managing the mutagen daemon lifecycle, this class:
  -  Starts the mutagen daemon using `mutagen daemon run`, with a `MUTAGEN_DATA_DIRECTORY` in `Application Support/Coder Desktop/Mutagen`, to avoid collisions with a system mutagen using `~/.mutagen`.
  -  Maintains a `gRPC` connection to the daemon socket.
  -  Stops the mutagen daemon over `gRPC`
  -  Relays stdout & stderr from the daemon, and watches if the process exits unexpectedly.
  -  Handles replacing an orphaned `mutagen daemon run` process if one exists.

This PR does not embed the mutagen binaries within the bundle, it just handles the case where they're present.


## Why is the file sync code in VPNLib?

When I had the FileSync code (namely protobuf definitions) in either:
- The app target
- A new `FSLib` framework target

Either the network extension crashed (in the first case) or the app crashed (in the second case) on launch.
The crash was super obtuse:
```
Library not loaded: @rpath/SwiftProtobuf.framework/Versions/A/SwiftProtobuf
```
especially considering `SwiftProtobuf` doesn't have a stable ABI and shouldn't be compiled as a framework.

At least one other person has ran into this issue when importing `SwiftProtobuf` multiple times:
https://github.com/apple/swift-protobuf/issues/1506#issuecomment-2435125065

Curiously, this also wasn't happening on local development builds (building and running via the XCode GUI), only when exporting via our build script.

### Solution

We're just going to overload `VPNLib` as the source of all our SwiftProtobuf & GRPC code. Since it's pretty big, and we don't want to embed it twice, we'll embed it once within the System Extension, and then have the app look for it in that bundle, see `LD_RUNPATH_SEARCH_PATHS`. It's not exactly ideal, but I don't think it's worth going to war with XCode over.


#### TODO
- [x] Replace the `Process` with https://github.com/jamf/Subprocess
---
 .swiftlint.yml                                |   4 +
 Coder Desktop/.swiftformat                    |   2 +-
 .../Coder Desktop/Coder_DesktopApp.swift      |  16 +-
 Coder Desktop/Coder Desktop/State.swift       |   1 +
 .../MenuState.swift}                          |   0
 .../{ => VPN}/NetworkExtension.swift          |   0
 .../Coder Desktop/{ => VPN}/VPNService.swift  |   0
 .../VPNSystemExtension.swift}                 |   0
 Coder Desktop/Resources/.gitkeep              |   0
 .../VPNLib/FileSync/FileSyncDaemon.swift      | 225 +++++++++++++
 .../VPNLib/FileSync/daemon.grpc.swift         | 299 ++++++++++++++++++
 Coder Desktop/VPNLib/FileSync/daemon.pb.swift |  83 +++++
 Coder Desktop/VPNLib/FileSync/daemon.proto    |  11 +
 Coder Desktop/project.yml                     |  25 +-
 Makefile                                      |  11 +-
 flake.lock                                    |  63 ++++
 flake.nix                                     |  15 +-
 17 files changed, 746 insertions(+), 9 deletions(-)
 create mode 100644 .swiftlint.yml
 rename Coder Desktop/Coder Desktop/{VPNMenuState.swift => VPN/MenuState.swift} (100%)
 rename Coder Desktop/Coder Desktop/{ => VPN}/NetworkExtension.swift (100%)
 rename Coder Desktop/Coder Desktop/{ => VPN}/VPNService.swift (100%)
 rename Coder Desktop/Coder Desktop/{SystemExtension.swift => VPN/VPNSystemExtension.swift} (100%)
 create mode 100644 Coder Desktop/Resources/.gitkeep
 create mode 100644 Coder Desktop/VPNLib/FileSync/FileSyncDaemon.swift
 create mode 100644 Coder Desktop/VPNLib/FileSync/daemon.grpc.swift
 create mode 100644 Coder Desktop/VPNLib/FileSync/daemon.pb.swift
 create mode 100644 Coder Desktop/VPNLib/FileSync/daemon.proto

diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 00000000..df9827ea
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,4 @@
+# TODO: Remove this once the grpc-swift-protobuf generator adds a lint disable comment
+excluded:
+  - "**/*.pb.swift"
+  - "**/*.grpc.swift"
\ No newline at end of file
diff --git a/Coder Desktop/.swiftformat b/Coder Desktop/.swiftformat
index cb200b40..b34aa3f1 100644
--- a/Coder Desktop/.swiftformat	
+++ b/Coder Desktop/.swiftformat	
@@ -1,3 +1,3 @@
 --selfrequired log,info,error,debug,critical,fault
---exclude **.pb.swift
+--exclude **.pb.swift,**.grpc.swift
 --condassignment always
\ No newline at end of file
diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
index f434e31d..1d379e91 100644
--- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift	
+++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift	
@@ -1,6 +1,7 @@
 import FluidMenuBarExtra
 import NetworkExtension
 import SwiftUI
+import VPNLib
 
 @main
 struct DesktopApp: App {
@@ -30,10 +31,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
     private var menuBar: MenuBarController?
     let vpn: CoderVPNService
     let state: AppState
+    let fileSyncDaemon: MutagenDaemon
 
     override init() {
         vpn = CoderVPNService()
         state = AppState(onChange: vpn.configureTunnelProviderProtocol)
+        fileSyncDaemon = MutagenDaemon()
     }
 
     func applicationDidFinishLaunching(_: Notification) {
@@ -56,14 +59,23 @@ class AppDelegate: NSObject, NSApplicationDelegate {
                 state.reconfigure()
             }
         }
+        // TODO: Start the daemon only once a file sync is configured
+        Task {
+            await fileSyncDaemon.start()
+        }
     }
 
     // This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
     // or return `.terminateNow`
     func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
-        if !state.stopVPNOnQuit { return .terminateNow }
         Task {
-            await vpn.stop()
+            async let vpnTask: Void = {
+                if await self.state.stopVPNOnQuit {
+                    await self.vpn.stop()
+                }
+            }()
+            async let fileSyncTask: Void = self.fileSyncDaemon.stop()
+            _ = await (vpnTask, fileSyncTask)
             NSApp.reply(toApplicationShouldTerminate: true)
         }
         return .terminateLater
diff --git a/Coder Desktop/Coder Desktop/State.swift b/Coder Desktop/Coder Desktop/State.swift
index a8404ff6..3e723c9f 100644
--- a/Coder Desktop/Coder Desktop/State.swift	
+++ b/Coder Desktop/Coder Desktop/State.swift	
@@ -4,6 +4,7 @@ import KeychainAccess
 import NetworkExtension
 import SwiftUI
 
+@MainActor
 class AppState: ObservableObject {
     let appId = Bundle.main.bundleIdentifier!
 
diff --git a/Coder Desktop/Coder Desktop/VPNMenuState.swift b/Coder Desktop/Coder Desktop/VPN/MenuState.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/VPNMenuState.swift
rename to Coder Desktop/Coder Desktop/VPN/MenuState.swift
diff --git a/Coder Desktop/Coder Desktop/NetworkExtension.swift b/Coder Desktop/Coder Desktop/VPN/NetworkExtension.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/NetworkExtension.swift
rename to Coder Desktop/Coder Desktop/VPN/NetworkExtension.swift
diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPN/VPNService.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/VPNService.swift
rename to Coder Desktop/Coder Desktop/VPN/VPNService.swift
diff --git a/Coder Desktop/Coder Desktop/SystemExtension.swift b/Coder Desktop/Coder Desktop/VPN/VPNSystemExtension.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/SystemExtension.swift
rename to Coder Desktop/Coder Desktop/VPN/VPNSystemExtension.swift
diff --git a/Coder Desktop/Resources/.gitkeep b/Coder Desktop/Resources/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/Coder Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder Desktop/VPNLib/FileSync/FileSyncDaemon.swift
new file mode 100644
index 00000000..9324c076
--- /dev/null
+++ b/Coder Desktop/VPNLib/FileSync/FileSyncDaemon.swift	
@@ -0,0 +1,225 @@
+import Foundation
+import GRPC
+import NIO
+import os
+import Subprocess
+
+@MainActor
+public protocol FileSyncDaemon: ObservableObject {
+    var state: DaemonState { get }
+    func start() async
+    func stop() async
+}
+
+@MainActor
+public class MutagenDaemon: FileSyncDaemon {
+    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen")
+
+    @Published public var state: DaemonState = .stopped {
+        didSet {
+            logger.info("daemon state changed: \(self.state.description, privacy: .public)")
+        }
+    }
+
+    private var mutagenProcess: Subprocess?
+    private let mutagenPath: URL!
+    private let mutagenDataDirectory: URL
+    private let mutagenDaemonSocket: URL
+
+    private var group: MultiThreadedEventLoopGroup?
+    private var channel: GRPCChannel?
+    private var client: Daemon_DaemonAsyncClient?
+
+    public init() {
+        #if arch(arm64)
+            mutagenPath = Bundle.main.url(forResource: "mutagen-darwin-arm64", withExtension: nil)
+        #elseif arch(x86_64)
+            mutagenPath = Bundle.main.url(forResource: "mutagen-darwin-amd64", withExtension: nil)
+        #else
+            fatalError("unknown architecture")
+        #endif
+        mutagenDataDirectory = FileManager.default.urls(
+            for: .applicationSupportDirectory,
+            in: .userDomainMask
+        ).first!.appending(path: "Coder Desktop").appending(path: "Mutagen")
+        mutagenDaemonSocket = mutagenDataDirectory.appending(path: "daemon").appending(path: "daemon.sock")
+        // It shouldn't be fatal if the app was built without Mutagen embedded,
+        // but file sync will be unavailable.
+        if mutagenPath == nil {
+            logger.warning("Mutagen not embedded in app, file sync will be unavailable")
+            state = .unavailable
+        }
+    }
+
+    public func start() async {
+        if case .unavailable = state { return }
+
+        // Stop an orphaned daemon, if there is one
+        try? await connect()
+        await stop()
+
+        mutagenProcess = createMutagenProcess()
+        // swiftlint:disable:next large_tuple
+        let (standardOutput, standardError, waitForExit): (Pipe.AsyncBytes, Pipe.AsyncBytes, @Sendable () async -> Void)
+        do {
+            (standardOutput, standardError, waitForExit) = try mutagenProcess!.run()
+        } catch {
+            state = .failed(DaemonError.daemonStartFailure(error))
+            return
+        }
+
+        Task {
+            await streamHandler(io: standardOutput)
+            logger.info("standard output stream closed")
+        }
+
+        Task {
+            await streamHandler(io: standardError)
+            logger.info("standard error stream closed")
+        }
+
+        Task {
+            await terminationHandler(waitForExit: waitForExit)
+        }
+
+        do {
+            try await connect()
+        } catch {
+            state = .failed(DaemonError.daemonStartFailure(error))
+            return
+        }
+
+        state = .running
+        logger.info(
+            """
+            mutagen daemon started, pid:
+             \(self.mutagenProcess?.pid.description ?? "unknown", privacy: .public)
+            """
+        )
+    }
+
+    private func connect() async throws(DaemonError) {
+        guard client == nil else {
+            // Already connected
+            return
+        }
+        group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+        do {
+            channel = try GRPCChannelPool.with(
+                target: .unixDomainSocket(mutagenDaemonSocket.path),
+                transportSecurity: .plaintext,
+                eventLoopGroup: group!
+            )
+            client = Daemon_DaemonAsyncClient(channel: channel!)
+            logger.info(
+                "Successfully connected to mutagen daemon, socket: \(self.mutagenDaemonSocket.path, privacy: .public)"
+            )
+        } catch {
+            logger.error("Failed to connect to gRPC: \(error)")
+            try? await cleanupGRPC()
+            throw DaemonError.connectionFailure(error)
+        }
+    }
+
+    private func cleanupGRPC() async throws {
+        try? await channel?.close().get()
+        try? await group?.shutdownGracefully()
+
+        client = nil
+        channel = nil
+        group = nil
+    }
+
+    public func stop() async {
+        if case .unavailable = state { return }
+        state = .stopped
+        guard FileManager.default.fileExists(atPath: mutagenDaemonSocket.path) else {
+            // Already stopped
+            return
+        }
+
+        // "We don't check the response or error, because the daemon
+        // may terminate before it has a chance to send the response."
+        _ = try? await client?.terminate(
+            Daemon_TerminateRequest(),
+            callOptions: .init(timeLimit: .timeout(.milliseconds(500)))
+        )
+
+        try? await cleanupGRPC()
+
+        mutagenProcess?.kill()
+        mutagenProcess = nil
+        logger.info("Daemon stopped and gRPC connection closed")
+    }
+
+    private func createMutagenProcess() -> Subprocess {
+        let process = Subprocess([mutagenPath.path, "daemon", "run"])
+        process.environment = [
+            "MUTAGEN_DATA_DIRECTORY": mutagenDataDirectory.path,
+        ]
+        logger.info("setting mutagen data directory: \(self.mutagenDataDirectory.path, privacy: .public)")
+        return process
+    }
+
+    private func terminationHandler(waitForExit: @Sendable () async -> Void) async {
+        await waitForExit()
+
+        switch state {
+        case .stopped:
+            logger.info("mutagen daemon stopped")
+        default:
+            logger.error(
+                """
+                mutagen daemon exited unexpectedly with code:
+                 \(self.mutagenProcess?.exitCode.description ?? "unknown")
+                """
+            )
+            state = .failed(.terminatedUnexpectedly)
+        }
+    }
+
+    private func streamHandler(io: Pipe.AsyncBytes) async {
+        for await line in io.lines {
+            logger.info("\(line, privacy: .public)")
+        }
+    }
+}
+
+public enum DaemonState {
+    case running
+    case stopped
+    case failed(DaemonError)
+    case unavailable
+
+    var description: String {
+        switch self {
+        case .running:
+            "Running"
+        case .stopped:
+            "Stopped"
+        case let .failed(error):
+            "Failed: \(error)"
+        case .unavailable:
+            "Unavailable"
+        }
+    }
+}
+
+public enum DaemonError: Error {
+    case daemonStartFailure(Error)
+    case connectionFailure(Error)
+    case terminatedUnexpectedly
+
+    var description: String {
+        switch self {
+        case let .daemonStartFailure(error):
+            "Daemon start failure: \(error)"
+        case let .connectionFailure(error):
+            "Connection failure: \(error)"
+        case .terminatedUnexpectedly:
+            "Daemon terminated unexpectedly"
+        }
+    }
+
+    var localizedDescription: String { description }
+}
diff --git a/Coder Desktop/VPNLib/FileSync/daemon.grpc.swift b/Coder Desktop/VPNLib/FileSync/daemon.grpc.swift
new file mode 100644
index 00000000..4fbe0789
--- /dev/null
+++ b/Coder Desktop/VPNLib/FileSync/daemon.grpc.swift	
@@ -0,0 +1,299 @@
+//
+// DO NOT EDIT.
+// swift-format-ignore-file
+//
+// Generated by the protocol buffer compiler.
+// Source: Coder Desktop/VPNLib/FileSync/daemon.proto
+//
+import GRPC
+import NIO
+import NIOConcurrencyHelpers
+import SwiftProtobuf
+
+
+/// Usage: instantiate `Daemon_DaemonClient`, then call methods of this protocol to make API calls.
+internal protocol Daemon_DaemonClientProtocol: GRPCClient {
+  var serviceName: String { get }
+  var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? { get }
+
+  func terminate(
+    _ request: Daemon_TerminateRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse>
+}
+
+extension Daemon_DaemonClientProtocol {
+  internal var serviceName: String {
+    return "daemon.Daemon"
+  }
+
+  /// Unary call to Terminate
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to Terminate.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func terminate(
+    _ request: Daemon_TerminateRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse> {
+    return self.makeUnaryCall(
+      path: Daemon_DaemonClientMetadata.Methods.terminate.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
+    )
+  }
+}
+
+@available(*, deprecated)
+extension Daemon_DaemonClient: @unchecked Sendable {}
+
+@available(*, deprecated, renamed: "Daemon_DaemonNIOClient")
+internal final class Daemon_DaemonClient: Daemon_DaemonClientProtocol {
+  private let lock = Lock()
+  private var _defaultCallOptions: CallOptions
+  private var _interceptors: Daemon_DaemonClientInterceptorFactoryProtocol?
+  internal let channel: GRPCChannel
+  internal var defaultCallOptions: CallOptions {
+    get { self.lock.withLock { return self._defaultCallOptions } }
+    set { self.lock.withLockVoid { self._defaultCallOptions = newValue } }
+  }
+  internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? {
+    get { self.lock.withLock { return self._interceptors } }
+    set { self.lock.withLockVoid { self._interceptors = newValue } }
+  }
+
+  /// Creates a client for the daemon.Daemon service.
+  ///
+  /// - Parameters:
+  ///   - channel: `GRPCChannel` to the service host.
+  ///   - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
+  ///   - interceptors: A factory providing interceptors for each RPC.
+  internal init(
+    channel: GRPCChannel,
+    defaultCallOptions: CallOptions = CallOptions(),
+    interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? = nil
+  ) {
+    self.channel = channel
+    self._defaultCallOptions = defaultCallOptions
+    self._interceptors = interceptors
+  }
+}
+
+internal struct Daemon_DaemonNIOClient: Daemon_DaemonClientProtocol {
+  internal var channel: GRPCChannel
+  internal var defaultCallOptions: CallOptions
+  internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol?
+
+  /// Creates a client for the daemon.Daemon service.
+  ///
+  /// - Parameters:
+  ///   - channel: `GRPCChannel` to the service host.
+  ///   - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
+  ///   - interceptors: A factory providing interceptors for each RPC.
+  internal init(
+    channel: GRPCChannel,
+    defaultCallOptions: CallOptions = CallOptions(),
+    interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? = nil
+  ) {
+    self.channel = channel
+    self.defaultCallOptions = defaultCallOptions
+    self.interceptors = interceptors
+  }
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+internal protocol Daemon_DaemonAsyncClientProtocol: GRPCClient {
+  static var serviceDescriptor: GRPCServiceDescriptor { get }
+  var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? { get }
+
+  func makeTerminateCall(
+    _ request: Daemon_TerminateRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse>
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension Daemon_DaemonAsyncClientProtocol {
+  internal static var serviceDescriptor: GRPCServiceDescriptor {
+    return Daemon_DaemonClientMetadata.serviceDescriptor
+  }
+
+  internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? {
+    return nil
+  }
+
+  internal func makeTerminateCall(
+    _ request: Daemon_TerminateRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Daemon_DaemonClientMetadata.Methods.terminate.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
+    )
+  }
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension Daemon_DaemonAsyncClientProtocol {
+  internal func terminate(
+    _ request: Daemon_TerminateRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Daemon_TerminateResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Daemon_DaemonClientMetadata.Methods.terminate.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
+    )
+  }
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+internal struct Daemon_DaemonAsyncClient: Daemon_DaemonAsyncClientProtocol {
+  internal var channel: GRPCChannel
+  internal var defaultCallOptions: CallOptions
+  internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol?
+
+  internal init(
+    channel: GRPCChannel,
+    defaultCallOptions: CallOptions = CallOptions(),
+    interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? = nil
+  ) {
+    self.channel = channel
+    self.defaultCallOptions = defaultCallOptions
+    self.interceptors = interceptors
+  }
+}
+
+internal protocol Daemon_DaemonClientInterceptorFactoryProtocol: Sendable {
+
+  /// - Returns: Interceptors to use when invoking 'terminate'.
+  func makeTerminateInterceptors() -> [ClientInterceptor<Daemon_TerminateRequest, Daemon_TerminateResponse>]
+}
+
+internal enum Daemon_DaemonClientMetadata {
+  internal static let serviceDescriptor = GRPCServiceDescriptor(
+    name: "Daemon",
+    fullName: "daemon.Daemon",
+    methods: [
+      Daemon_DaemonClientMetadata.Methods.terminate,
+    ]
+  )
+
+  internal enum Methods {
+    internal static let terminate = GRPCMethodDescriptor(
+      name: "Terminate",
+      path: "/daemon.Daemon/Terminate",
+      type: GRPCCallType.unary
+    )
+  }
+}
+
+/// To build a server, implement a class that conforms to this protocol.
+internal protocol Daemon_DaemonProvider: CallHandlerProvider {
+  var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? { get }
+
+  func terminate(request: Daemon_TerminateRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Daemon_TerminateResponse>
+}
+
+extension Daemon_DaemonProvider {
+  internal var serviceName: Substring {
+    return Daemon_DaemonServerMetadata.serviceDescriptor.fullName[...]
+  }
+
+  /// Determines, calls and returns the appropriate request handler, depending on the request's method.
+  /// Returns nil for methods not handled by this service.
+  internal func handle(
+    method name: Substring,
+    context: CallHandlerContext
+  ) -> GRPCServerHandlerProtocol? {
+    switch name {
+    case "Terminate":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Daemon_TerminateRequest>(),
+        responseSerializer: ProtobufSerializer<Daemon_TerminateResponse>(),
+        interceptors: self.interceptors?.makeTerminateInterceptors() ?? [],
+        userFunction: self.terminate(request:context:)
+      )
+
+    default:
+      return nil
+    }
+  }
+}
+
+/// To implement a server, implement an object which conforms to this protocol.
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+internal protocol Daemon_DaemonAsyncProvider: CallHandlerProvider, Sendable {
+  static var serviceDescriptor: GRPCServiceDescriptor { get }
+  var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? { get }
+
+  func terminate(
+    request: Daemon_TerminateRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Daemon_TerminateResponse
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension Daemon_DaemonAsyncProvider {
+  internal static var serviceDescriptor: GRPCServiceDescriptor {
+    return Daemon_DaemonServerMetadata.serviceDescriptor
+  }
+
+  internal var serviceName: Substring {
+    return Daemon_DaemonServerMetadata.serviceDescriptor.fullName[...]
+  }
+
+  internal var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? {
+    return nil
+  }
+
+  internal func handle(
+    method name: Substring,
+    context: CallHandlerContext
+  ) -> GRPCServerHandlerProtocol? {
+    switch name {
+    case "Terminate":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Daemon_TerminateRequest>(),
+        responseSerializer: ProtobufSerializer<Daemon_TerminateResponse>(),
+        interceptors: self.interceptors?.makeTerminateInterceptors() ?? [],
+        wrapping: { try await self.terminate(request: $0, context: $1) }
+      )
+
+    default:
+      return nil
+    }
+  }
+}
+
+internal protocol Daemon_DaemonServerInterceptorFactoryProtocol: Sendable {
+
+  /// - Returns: Interceptors to use when handling 'terminate'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makeTerminateInterceptors() -> [ServerInterceptor<Daemon_TerminateRequest, Daemon_TerminateResponse>]
+}
+
+internal enum Daemon_DaemonServerMetadata {
+  internal static let serviceDescriptor = GRPCServiceDescriptor(
+    name: "Daemon",
+    fullName: "daemon.Daemon",
+    methods: [
+      Daemon_DaemonServerMetadata.Methods.terminate,
+    ]
+  )
+
+  internal enum Methods {
+    internal static let terminate = GRPCMethodDescriptor(
+      name: "Terminate",
+      path: "/daemon.Daemon/Terminate",
+      type: GRPCCallType.unary
+    )
+  }
+}
diff --git a/Coder Desktop/VPNLib/FileSync/daemon.pb.swift b/Coder Desktop/VPNLib/FileSync/daemon.pb.swift
new file mode 100644
index 00000000..4ed73c69
--- /dev/null
+++ b/Coder Desktop/VPNLib/FileSync/daemon.pb.swift	
@@ -0,0 +1,83 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: Coder Desktop/VPNLib/FileSync/daemon.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+struct Daemon_TerminateRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+struct Daemon_TerminateResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "daemon"
+
+extension Daemon_TerminateRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".TerminateRequest"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Daemon_TerminateRequest, rhs: Daemon_TerminateRequest) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Daemon_TerminateResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".TerminateResponse"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Daemon_TerminateResponse, rhs: Daemon_TerminateResponse) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder Desktop/VPNLib/FileSync/daemon.proto b/Coder Desktop/VPNLib/FileSync/daemon.proto
new file mode 100644
index 00000000..4431b35d
--- /dev/null
+++ b/Coder Desktop/VPNLib/FileSync/daemon.proto	
@@ -0,0 +1,11 @@
+syntax = "proto3";
+
+package daemon;
+
+message TerminateRequest{}
+
+message TerminateResponse{}
+
+service Daemon {
+    rpc Terminate(TerminateRequest) returns (TerminateResponse) {}
+}
diff --git a/Coder Desktop/project.yml b/Coder Desktop/project.yml
index 2872515b..4b0eef6d 100644
--- a/Coder Desktop/project.yml	
+++ b/Coder Desktop/project.yml	
@@ -5,6 +5,9 @@ options:
     macOS: "14.0"
   xcodeVersion: "1600"
   minimumXcodeGenVersion: "2.42.0"
+  fileTypes:
+    proto:
+      buildPhase: none
 
 settings:
   base:
@@ -105,6 +108,13 @@ packages:
   LaunchAtLogin:
     url: https://github.com/sindresorhus/LaunchAtLogin-modern
     from: 1.1.0
+  GRPC:
+    url: https://github.com/grpc/grpc-swift
+    # v2 does not support macOS 14.0
+    exactVersion: 1.24.2
+  Subprocess:
+    url: https://github.com/jamf/Subprocess
+    revision: 9d67b79 
 
 targets:
   Coder Desktop:
@@ -112,6 +122,8 @@ targets:
     platform: macOS
     sources:
       - path: Coder Desktop
+      - path: Resources
+        buildPhase: resources
     entitlements:
       path: Coder Desktop/Coder_Desktop.entitlements
       properties:
@@ -145,11 +157,16 @@ targets:
         DSTROOT: $(LOCAL_APPS_DIR)/Coder
         INSTALL_PATH: /
         SKIP_INSTALL: NO
+        LD_RUNPATH_SEARCH_PATHS:
+          # Load frameworks from the SE bundle.
+          - "@executable_path/../../Contents/Library/SystemExtensions/com.coder.Coder-Desktop.VPN.systemextension/Contents/Frameworks" 
+          - "@executable_path/../Frameworks"
+          - "@loader_path/Frameworks"
     dependencies:
       - target: CoderSDK
-        embed: true
+        embed: false # Loaded from SE bundle
       - target: VPNLib
-        embed: true
+        embed: false # Loaded from SE bundle
       - target: VPN
         embed: without-signing # Embed without signing.
       - package: FluidMenuBarExtra
@@ -224,8 +241,10 @@ targets:
         # Empty outside of release builds
         PROVISIONING_PROFILE_SPECIFIER: ${EXT_PROVISIONING_PROFILE_ID}
     dependencies:
+      # The app loads the framework embedded here too
       - target: VPNLib
         embed: true
+      # The app loads the framework embedded here too
       - target: CoderSDK
         embed: true
       - sdk: NetworkExtension.framework
@@ -253,6 +272,8 @@ targets:
       - package: SwiftProtobuf
       - package: SwiftProtobuf
         product: SwiftProtobufPluginLibrary
+      - package: GRPC
+      - package: Subprocess
       - target: CoderSDK
         embed: false
 
diff --git a/Makefile b/Makefile
index e823a133..f31e8b11 100644
--- a/Makefile
+++ b/Makefile
@@ -33,7 +33,8 @@ APP_SIGNING_KEYCHAIN := $(if $(wildcard $(KEYCHAIN_FILE)),$(shell realpath $(KEY
 .PHONY: setup
 setup: \
 	$(XCPROJECT) \
-	$(PROJECT)/VPNLib/vpn.pb.swift
+	$(PROJECT)/VPNLib/vpn.pb.swift \
+	$(PROJECT)/VPNLib/FileSync/daemon.pb.swift
 
 $(XCPROJECT): $(PROJECT)/project.yml
 	cd $(PROJECT); \
@@ -48,6 +49,12 @@ $(XCPROJECT): $(PROJECT)/project.yml
 $(PROJECT)/VPNLib/vpn.pb.swift: $(PROJECT)/VPNLib/vpn.proto
 	protoc --swift_opt=Visibility=public --swift_out=. 'Coder Desktop/VPNLib/vpn.proto'
 
+$(PROJECT)/VPNLib/FileSync/daemon.pb.swift: $(PROJECT)/VPNLib/FileSync/daemon.proto
+	protoc \
+		--swift_out=.\
+		--grpc-swift_out=. \
+		'Coder Desktop/VPNLib/FileSync/daemon.proto'
+
 $(KEYCHAIN_FILE):
 	security create-keychain -p "" "$(APP_SIGNING_KEYCHAIN)"
 	security set-keychain-settings -lut 21600 "$(APP_SIGNING_KEYCHAIN)"
@@ -130,7 +137,7 @@ clean/build:
 	rm -rf build/ release/ $$out
 
 .PHONY: proto
-proto: $(PROJECT)/VPNLib/vpn.pb.swift ## Generate Swift files from protobufs
+proto: $(PROJECT)/VPNLib/vpn.pb.swift $(PROJECT)/VPNLib/FileSync/daemon.pb.swift ## Generate Swift files from protobufs
 
 .PHONY: help
 help: ## Show this help
diff --git a/flake.lock b/flake.lock
index b5b74155..011c0d0a 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,5 +1,25 @@
 {
   "nodes": {
+    "flake-parts": {
+      "inputs": {
+        "nixpkgs-lib": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1741352980,
+        "narHash": "sha256-+u2UunDA4Cl5Fci3m7S643HzKmIDAe+fiXrLqYsR2fs=",
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "rev": "f4330d22f1c5d2ba72d3d22df5597d123fdb60a9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "type": "github"
+      }
+    },
     "flake-utils": {
       "inputs": {
         "systems": "systems"
@@ -18,6 +38,47 @@
         "type": "github"
       }
     },
+    "grpc-swift": {
+      "inputs": {
+        "flake-parts": [
+          "flake-parts"
+        ],
+        "grpc-swift-src": "grpc-swift-src",
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1734611727,
+        "narHash": "sha256-HWyTCVTAZ+R2fmK6+FoG72U1f7srF6dqaZJANsd1heE=",
+        "owner": "i10416",
+        "repo": "grpc-swift-flake",
+        "rev": "b3e21ab4c686be29af42ccd36c4cc476a1ccbd8e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "i10416",
+        "repo": "grpc-swift-flake",
+        "type": "github"
+      }
+    },
+    "grpc-swift-src": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1726668274,
+        "narHash": "sha256-uI8MpRIGGn/d00pNzBxEZgQ06Q9Ladvdlc5cGNhOnkI=",
+        "owner": "grpc",
+        "repo": "grpc-swift",
+        "rev": "07123ed731671e800ab8d641006613612e954746",
+        "type": "github"
+      },
+      "original": {
+        "owner": "grpc",
+        "ref": "refs/tags/1.23.1",
+        "repo": "grpc-swift",
+        "type": "github"
+      }
+    },
     "nixpkgs": {
       "locked": {
         "lastModified": 1740560979,
@@ -36,7 +97,9 @@
     },
     "root": {
       "inputs": {
+        "flake-parts": "flake-parts",
         "flake-utils": "flake-utils",
+        "grpc-swift": "grpc-swift",
         "nixpkgs": "nixpkgs"
       }
     },
diff --git a/flake.nix b/flake.nix
index 0b097536..ab3ab0a1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -4,13 +4,23 @@
   inputs = {
     nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
     flake-utils.url = "github:numtide/flake-utils";
+    flake-parts = {
+      url = "github:hercules-ci/flake-parts";
+      inputs.nixpkgs-lib.follows = "nixpkgs";
+    };
+    grpc-swift = {
+      url = "github:i10416/grpc-swift-flake";
+      inputs.nixpkgs.follows = "nixpkgs";
+      inputs.flake-parts.follows = "flake-parts";
+    };
   };
 
   outputs =
     {
-      self,
       nixpkgs,
       flake-utils,
+      grpc-swift,
+      ...
     }:
     flake-utils.lib.eachSystem
       (with flake-utils.lib.system; [
@@ -40,7 +50,8 @@
                 git
                 gnumake
                 protobuf_28
-                protoc-gen-swift
+                grpc-swift.packages.${system}.protoc-gen-grpc-swift
+                grpc-swift.packages.${system}.protoc-gen-swift
                 swiftformat
                 swiftlint
                 xcbeautify

From 93e7a8f2b1536b753f69292c2d915f4a35eeb7e7 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 12 Mar 2025 16:46:22 +1100
Subject: [PATCH 04/25] ci: download mutagen binary into resources (#103)

Closes https://github.com/coder/coder-desktop-macos/issues/60
---
 .github/workflows/release.yml           | 20 +++++++++++++-
 .gitignore                              |  3 +++
 Coder Desktop/Resources/.gitkeep        |  0
 Coder Desktop/Resources/.mutagenversion |  1 +
 Makefile                                | 36 +++++++++++++++++++++----
 5 files changed, 54 insertions(+), 6 deletions(-)
 delete mode 100644 Coder Desktop/Resources/.gitkeep
 create mode 100644 Coder Desktop/Resources/.mutagenversion

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ebe8e9c0..c86eb175 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,6 +7,13 @@ on:
   release:
     types: [published]
 
+  workflow_dispatch:
+    inputs:
+      dryrun:
+        description: 'Run in dry-run mode (upload as artifact instead of release asset)'
+        required: true
+        type: boolean
+        default: false
 permissions: {}
 
 # Cancel in-progress runs for when multiple PRs get merged
@@ -51,7 +58,18 @@ jobs:
           EXT_PROF: ${{ secrets.CODER_DESKTOP_EXTENSION_PROVISIONPROFILE_B64 }}
         run: make release
 
+      # Upload as artifact in dry-run mode
+      - name: Upload Build Artifact
+        if: ${{ inputs.dryrun }}
+        uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
+        with:
+          name: coder-desktop-build
+          path: ${{ github.workspace }}/outputs/out
+          retention-days: 7
+
+      # Upload to release in non-dry-run mode
       - name: Upload Release Assets
+        if: ${{ !inputs.dryrun }}
         run: gh release upload "$RELEASE_TAG" "$out"/* --clobber
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -60,7 +78,7 @@ jobs:
   update-cask:
     name: Update homebrew-coder cask
     runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest'}}
-    if: ${{ github.repository_owner == 'coder' }}
+    if: ${{ github.repository_owner == 'coder' && !inputs.dryrun }}
     needs: build
     steps:
       - name: Checkout
diff --git a/.gitignore b/.gitignore
index e6983d3b..a1b91af5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -302,3 +302,6 @@ release/
 
 # marker files
 .fl5C1A396C
+
+# Embedded mutagen resources
+Coder Desktop/Resources/mutagen-*
diff --git a/Coder Desktop/Resources/.gitkeep b/Coder Desktop/Resources/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/Coder Desktop/Resources/.mutagenversion b/Coder Desktop/Resources/.mutagenversion
new file mode 100644
index 00000000..f3a5a576
--- /dev/null
+++ b/Coder Desktop/Resources/.mutagenversion	
@@ -0,0 +1 @@
+v0.18.1
diff --git a/Makefile b/Makefile
index f31e8b11..259c1ce5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,11 @@
+# Use bash, and immediately exit on failure
+SHELL := bash
+.SHELLFLAGS := -ceu
+
+# This doesn't work on directories.
+# See https://stackoverflow.com/questions/25752543/make-delete-on-error-for-directory-targets
+.DELETE_ON_ERROR:
+
 ifdef CI
 LINTFLAGS := --reporter github-actions-logging
 FMTFLAGS := --lint --reporter github-actions-log
@@ -11,18 +19,26 @@ XCPROJECT := Coder\ Desktop/Coder\ Desktop.xcodeproj
 SCHEME := Coder\ Desktop
 SWIFT_VERSION := 6.0
 
+MUTAGEN_RESOURCES := mutagen-agents.tar.gz mutagen-darwin-arm64 mutagen-darwin-amd64
+ifndef MUTAGEN_VERSION
+MUTAGEN_VERSION:=$(shell grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' $(PROJECT)/Resources/.mutagenversion)
+endif
+ifeq ($(strip $(MUTAGEN_VERSION)),)
+$(error MUTAGEN_VERSION must be a valid version)
+endif
+
 ifndef CURRENT_PROJECT_VERSION
-	CURRENT_PROJECT_VERSION:=$(shell git describe --match 'v[0-9]*' --dirty='.devel' --always --tags)
+CURRENT_PROJECT_VERSION:=$(shell git describe --match 'v[0-9]*' --dirty='.devel' --always --tags)
 endif
 ifeq ($(strip $(CURRENT_PROJECT_VERSION)),)
-	$(error CURRENT_PROJECT_VERSION cannot be empty)
+$(error CURRENT_PROJECT_VERSION cannot be empty)
 endif
 
 ifndef MARKETING_VERSION
-	MARKETING_VERSION:=$(shell git describe --match 'v[0-9]*' --tags --abbrev=0 | sed 's/^v//' | sed 's/-.*$$//')
+MARKETING_VERSION:=$(shell git describe --match 'v[0-9]*' --tags --abbrev=0 | sed 's/^v//' | sed 's/-.*$$//')
 endif
 ifeq ($(strip $(MARKETING_VERSION)),)
-	$(error MARKETING_VERSION cannot be empty)
+$(error MARKETING_VERSION cannot be empty)
 endif
 
 # Define the keychain file name first
@@ -32,10 +48,16 @@ APP_SIGNING_KEYCHAIN := $(if $(wildcard $(KEYCHAIN_FILE)),$(shell realpath $(KEY
 
 .PHONY: setup
 setup: \
+	$(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)) \
 	$(XCPROJECT) \
 	$(PROJECT)/VPNLib/vpn.pb.swift \
 	$(PROJECT)/VPNLib/FileSync/daemon.pb.swift
 
+# Mutagen resources
+$(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)): $(PROJECT)/Resources/.mutagenversion
+	curl -sL "https://storage.googleapis.com/coder-desktop/mutagen/$(MUTAGEN_VERSION)/$$(basename "$@")" -o "$@"
+	chmod +x "$@"
+
 $(XCPROJECT): $(PROJECT)/project.yml
 	cd $(PROJECT); \
 		SWIFT_VERSION=$(SWIFT_VERSION) \
@@ -113,7 +135,7 @@ lint/actions: ## Lint GitHub Actions
 	zizmor .
 
 .PHONY: clean
-clean: clean/project clean/keychain clean/build ## Clean project and artifacts
+clean: clean/project clean/keychain clean/build clean/mutagen ## Clean project and artifacts
 
 .PHONY: clean/project
 clean/project:
@@ -136,6 +158,10 @@ clean/keychain:
 clean/build:
 	rm -rf build/ release/ $$out
 
+.PHONY: clean/mutagen
+clean/mutagen:
+	find $(PROJECT)/Resources -name 'mutagen-*' -delete
+
 .PHONY: proto
 proto: $(PROJECT)/VPNLib/vpn.pb.swift $(PROJECT)/VPNLib/FileSync/daemon.pb.swift ## Generate Swift files from protobufs
 

From e8e4004e41e44b7fb8989d105d0b250245afb0df Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Thu, 13 Mar 2025 13:54:24 +1100
Subject: [PATCH 05/25] refactor: replace spaces with hyphens in directory
 names (#110)

Builds, passes tests, and a release build works: https://github.com/coder/coder-desktop-macos/actions/runs/13804258363

The main app target is still `Coder Desktop`, just not the names of any directories.
---
 .gitignore                                    |   2 +-
 CONTRIBUTING.md                               |   4 ++--
 {Coder Desktop => Coder-Desktop}/.swiftformat |   0
 .../.swiftlint.yml                            |   0
 .../Coder-Desktop.xctestplan                  |  14 +++++------
 .../Coder-Desktop}/About.swift                |   0
 .../AccentColor.colorset/Contents.json        |   0
 .../AppIcon.appiconset/1024.png               | Bin
 .../AppIcon.appiconset/128.png                | Bin
 .../Assets.xcassets/AppIcon.appiconset/16.png | Bin
 .../AppIcon.appiconset/256.png                | Bin
 .../Assets.xcassets/AppIcon.appiconset/32.png | Bin
 .../AppIcon.appiconset/512.png                | Bin
 .../Assets.xcassets/AppIcon.appiconset/64.png | Bin
 .../AppIcon.appiconset/Contents.json          |   0
 .../Assets.xcassets/Contents.json             |   0
 .../MenuBarIcon.imageset/Contents.json        |   0
 .../MenuBarIcon.imageset/coder_icon_16.png    | Bin
 .../coder_icon_16_dark.png                    | Bin
 .../MenuBarIcon.imageset/coder_icon_32.png    | Bin
 .../coder_icon_32_dark.png                    | Bin
 .../Coder-Desktop}/Coder_DesktopApp.swift     |   0
 .../Coder-Desktop}/Info.plist                 |   0
 .../MenuBarIconController.swift               |   0
 .../Preview Assets.xcassets/Contents.json     |   0
 .../Preview Content/PreviewVPN.swift          |   0
 .../Coder-Desktop}/State.swift                |   0
 .../Coder-Desktop}/Theme.swift                |   0
 .../Coder-Desktop}/VPN/MenuState.swift        |   0
 .../Coder-Desktop}/VPN/NetworkExtension.swift |   0
 .../Coder-Desktop}/VPN/VPNService.swift       |   0
 .../VPN/VPNSystemExtension.swift              |   0
 .../Coder-Desktop}/Views/Agents.swift         |   0
 .../Coder-Desktop}/Views/AuthButton.swift     |   0
 .../Coder-Desktop}/Views/ButtonRow.swift      |   0
 .../Coder-Desktop}/Views/InvalidAgents.swift  |   0
 .../Coder-Desktop}/Views/LoginForm.swift      |   0
 .../Coder-Desktop}/Views/ResponsiveLink.swift |   0
 .../Views/Settings/GeneralTab.swift           |   0
 .../Views/Settings/LiteralHeaderModal.swift   |   0
 .../Settings/LiteralHeadersSection.swift      |   0
 .../Views/Settings/NetworkTab.swift           |   0
 .../Views/Settings/Settings.swift             |   0
 .../Coder-Desktop}/Views/TrayDivider.swift    |   0
 .../Coder-Desktop}/Views/Util.swift           |   0
 .../Coder-Desktop}/Views/VPNMenu.swift        |   0
 .../Coder-Desktop}/Views/VPNMenuItem.swift    |   0
 .../Coder-Desktop}/Views/VPNState.swift       |   0
 .../Coder-Desktop}/Windows.swift              |   0
 .../Coder-Desktop}/XPCInterface.swift         |   0
 .../Coder-DesktopTests}/AgentsTests.swift     |   0
 .../LiteralHeadersSettingTests.swift          |   0
 .../Coder-DesktopTests}/LoginFormTests.swift  |   0
 .../Coder-DesktopTests}/Util.swift            |   0
 .../VPNMenuStateTests.swift                   |   0
 .../Coder-DesktopTests}/VPNMenuTests.swift    |   0
 .../Coder-DesktopTests}/VPNStateTests.swift   |   0
 .../Coder_DesktopUITests.swift                |   0
 .../Coder_DesktopUITestsLaunchTests.swift     |   0
 .../CoderSDK/Client.swift                     |   0
 .../CoderSDK/CoderSDK.h                       |   0
 .../CoderSDK/Date.swift                       |   0
 .../CoderSDK/Deployment.swift                 |   0
 .../CoderSDK/HTTP.swift                       |   0
 .../CoderSDK/User.swift                       |   0
 .../CoderSDKTests/CoderSDKTests.swift         |   0
 .../Resources/.mutagenversion                 |   0
 .../VPN/Info.plist                            |   0
 .../VPN/Manager.swift                         |   0
 .../VPN/PacketTunnelProvider.swift            |   0
 .../VPN/TunnelHandle.swift                    |   0
 .../VPN/XPCInterface.swift                    |   0
 ..._coder_Coder_Desktop_VPN-Bridging-Header.h |   0
 .../VPN/main.swift                            |   0
 .../VPNLib/Convert.swift                      |   0
 .../VPNLib/Download.swift                     |   0
 .../VPNLib/FileSync/FileSyncDaemon.swift      |   0
 .../VPNLib/FileSync/daemon.grpc.swift         |   2 +-
 .../VPNLib/FileSync/daemon.pb.swift           |   2 +-
 .../VPNLib/FileSync/daemon.proto              |   0
 .../VPNLib/Receiver.swift                     |   0
 .../VPNLib/Sender.swift                       |   0
 .../VPNLib/Speaker.swift                      |   0
 .../VPNLib/Util.swift                         |   0
 .../VPNLib/VPNLib.h                           |   0
 .../VPNLib/XPC.swift                          |   0
 .../VPNLib/vpn.pb.swift                       |   2 +-
 .../VPNLib/vpn.proto                          |   0
 .../VPNLibTests/ConvertTests.swift            |   0
 .../VPNLibTests/DownloadTests.swift           |   0
 .../VPNLibTests/ProtoTests.swift              |   0
 .../VPNLibTests/SpeakerTests.swift            |   0
 {Coder Desktop => Coder-Desktop}/project.yml  |  22 +++++++++---------
 Makefile                                      |  15 ++++++------
 scripts/build.sh                              |   6 ++---
 scripts/update-cask.sh                        |  10 ++++----
 96 files changed, 40 insertions(+), 39 deletions(-)
 rename {Coder Desktop => Coder-Desktop}/.swiftformat (100%)
 rename {Coder Desktop => Coder-Desktop}/.swiftlint.yml (100%)
 rename Coder Desktop/Coder Desktop.xctestplan => Coder-Desktop/Coder-Desktop.xctestplan (68%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/About.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/AccentColor.colorset/Contents.json (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/AppIcon.appiconset/1024.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/AppIcon.appiconset/128.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/AppIcon.appiconset/16.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/AppIcon.appiconset/256.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/AppIcon.appiconset/32.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/AppIcon.appiconset/512.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/AppIcon.appiconset/64.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/Contents.json (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/MenuBarIcon.imageset/Contents.json (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16_dark.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32_dark.png (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Coder_DesktopApp.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Info.plist (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/MenuBarIconController.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Preview Content/Preview Assets.xcassets/Contents.json (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Preview Content/PreviewVPN.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/State.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Theme.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/VPN/MenuState.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/VPN/NetworkExtension.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/VPN/VPNService.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/VPN/VPNSystemExtension.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/Agents.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/AuthButton.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/ButtonRow.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/InvalidAgents.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/LoginForm.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/ResponsiveLink.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/Settings/GeneralTab.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/Settings/LiteralHeaderModal.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/Settings/LiteralHeadersSection.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/Settings/NetworkTab.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/Settings/Settings.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/TrayDivider.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/Util.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/VPNMenu.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/VPNMenuItem.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Views/VPNState.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/Windows.swift (100%)
 rename {Coder Desktop/Coder Desktop => Coder-Desktop/Coder-Desktop}/XPCInterface.swift (100%)
 rename {Coder Desktop/Coder DesktopTests => Coder-Desktop/Coder-DesktopTests}/AgentsTests.swift (100%)
 rename {Coder Desktop/Coder DesktopTests => Coder-Desktop/Coder-DesktopTests}/LiteralHeadersSettingTests.swift (100%)
 rename {Coder Desktop/Coder DesktopTests => Coder-Desktop/Coder-DesktopTests}/LoginFormTests.swift (100%)
 rename {Coder Desktop/Coder DesktopTests => Coder-Desktop/Coder-DesktopTests}/Util.swift (100%)
 rename {Coder Desktop/Coder DesktopTests => Coder-Desktop/Coder-DesktopTests}/VPNMenuStateTests.swift (100%)
 rename {Coder Desktop/Coder DesktopTests => Coder-Desktop/Coder-DesktopTests}/VPNMenuTests.swift (100%)
 rename {Coder Desktop/Coder DesktopTests => Coder-Desktop/Coder-DesktopTests}/VPNStateTests.swift (100%)
 rename {Coder Desktop/Coder DesktopUITests => Coder-Desktop/Coder-DesktopUITests}/Coder_DesktopUITests.swift (100%)
 rename {Coder Desktop/Coder DesktopUITests => Coder-Desktop/Coder-DesktopUITests}/Coder_DesktopUITestsLaunchTests.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/CoderSDK/Client.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/CoderSDK/CoderSDK.h (100%)
 rename {Coder Desktop => Coder-Desktop}/CoderSDK/Date.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/CoderSDK/Deployment.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/CoderSDK/HTTP.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/CoderSDK/User.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/CoderSDKTests/CoderSDKTests.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/Resources/.mutagenversion (100%)
 rename {Coder Desktop => Coder-Desktop}/VPN/Info.plist (100%)
 rename {Coder Desktop => Coder-Desktop}/VPN/Manager.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPN/PacketTunnelProvider.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPN/TunnelHandle.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPN/XPCInterface.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPN/com_coder_Coder_Desktop_VPN-Bridging-Header.h (100%)
 rename {Coder Desktop => Coder-Desktop}/VPN/main.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/Convert.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/Download.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/FileSync/FileSyncDaemon.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/FileSync/daemon.grpc.swift (99%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/FileSync/daemon.pb.swift (98%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/FileSync/daemon.proto (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/Receiver.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/Sender.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/Speaker.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/Util.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/VPNLib.h (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/XPC.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/vpn.pb.swift (99%)
 rename {Coder Desktop => Coder-Desktop}/VPNLib/vpn.proto (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLibTests/ConvertTests.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLibTests/DownloadTests.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLibTests/ProtoTests.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/VPNLibTests/SpeakerTests.swift (100%)
 rename {Coder Desktop => Coder-Desktop}/project.yml (96%)

diff --git a/.gitignore b/.gitignore
index a1b91af5..45340d37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -304,4 +304,4 @@ release/
 .fl5C1A396C
 
 # Embedded mutagen resources
-Coder Desktop/Resources/mutagen-*
+Coder-Desktop/Resources/mutagen-*
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index cec0dfe5..7b01b61a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -77,7 +77,7 @@ make
 ```
 
 This will use **XcodeGen** to create the required Xcode project files.
-The configuration for the project is defined in `Coder Desktop/project.yml`.
+The configuration for the project is defined in `Coder-Desktop/project.yml`.
 
 ## Common Make Commands
 
@@ -96,7 +96,7 @@ For continuous development, you can also use:
 make watch-gen
 ```
 
-This command watches for changes to `Coder Desktop/project.yml` and regenerates
+This command watches for changes to `Coder-Desktop/project.yml` and regenerates
 the Xcode project file as needed.
 
 ## Testing and Formatting
diff --git a/Coder Desktop/.swiftformat b/Coder-Desktop/.swiftformat
similarity index 100%
rename from Coder Desktop/.swiftformat
rename to Coder-Desktop/.swiftformat
diff --git a/Coder Desktop/.swiftlint.yml b/Coder-Desktop/.swiftlint.yml
similarity index 100%
rename from Coder Desktop/.swiftlint.yml
rename to Coder-Desktop/.swiftlint.yml
diff --git a/Coder Desktop/Coder Desktop.xctestplan b/Coder-Desktop/Coder-Desktop.xctestplan
similarity index 68%
rename from Coder Desktop/Coder Desktop.xctestplan
rename to Coder-Desktop/Coder-Desktop.xctestplan
index a0f608b9..0ddb4e11 100644
--- a/Coder Desktop/Coder Desktop.xctestplan	
+++ b/Coder-Desktop/Coder-Desktop.xctestplan
@@ -10,7 +10,7 @@
   ],
   "defaultOptions" : {
     "targetForVariableExpansion" : {
-      "containerPath" : "container:Coder Desktop.xcodeproj",
+      "containerPath" : "container:Coder-Desktop.xcodeproj",
       "identifier" : "961678FB2CFF100D00B2B6DF",
       "name" : "Coder Desktop"
     }
@@ -18,7 +18,7 @@
   "testTargets" : [
     {
       "target" : {
-        "containerPath" : "container:Coder Desktop.xcodeproj",
+        "containerPath" : "container:Coder-Desktop.xcodeproj",
         "identifier" : "AA3B40972D2FC8560099996A",
         "name" : "CoderSDKTests"
       }
@@ -27,23 +27,23 @@
       "enabled" : false,
       "parallelizable" : true,
       "target" : {
-        "containerPath" : "container:Coder Desktop.xcodeproj",
+        "containerPath" : "container:Coder-Desktop.xcodeproj",
         "identifier" : "961679182CFF100E00B2B6DF",
-        "name" : "Coder DesktopUITests"
+        "name" : "Coder-DesktopUITests"
       }
     },
     {
       "target" : {
-        "containerPath" : "container:Coder Desktop.xcodeproj",
+        "containerPath" : "container:Coder-Desktop.xcodeproj",
         "identifier" : "AA3B3DA72D2D23860099996A",
         "name" : "VPNLibTests"
       }
     },
     {
       "target" : {
-        "containerPath" : "container:Coder Desktop.xcodeproj",
+        "containerPath" : "container:Coder-Desktop.xcodeproj",
         "identifier" : "9616790E2CFF100E00B2B6DF",
-        "name" : "Coder DesktopTests"
+        "name" : "Coder-DesktopTests"
       }
     }
   ],
diff --git a/Coder Desktop/Coder Desktop/About.swift b/Coder-Desktop/Coder-Desktop/About.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/About.swift
rename to Coder-Desktop/Coder-Desktop/About.swift
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/AccentColor.colorset/Contents.json b/Coder-Desktop/Coder-Desktop/Assets.xcassets/AccentColor.colorset/Contents.json
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/AccentColor.colorset/Contents.json
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/AccentColor.colorset/Contents.json
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/1024.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/1024.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/1024.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/1024.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/128.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/128.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/128.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/128.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/16.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/16.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/16.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/16.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/256.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/256.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/256.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/256.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/32.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/32.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/32.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/32.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/512.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/512.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/512.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/512.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/64.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/64.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/64.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/64.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/Contents.json b/Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/AppIcon.appiconset/Contents.json
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/AppIcon.appiconset/Contents.json
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/Contents.json b/Coder-Desktop/Coder-Desktop/Assets.xcassets/Contents.json
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/Contents.json
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/Contents.json
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/Contents.json b/Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/Contents.json
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/Contents.json
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/Contents.json
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16_dark.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16_dark.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16_dark.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_16_dark.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32.png
diff --git a/Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32_dark.png b/Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32_dark.png
similarity index 100%
rename from Coder Desktop/Coder Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32_dark.png
rename to Coder-Desktop/Coder-Desktop/Assets.xcassets/MenuBarIcon.imageset/coder_icon_32_dark.png
diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
rename to Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
diff --git a/Coder Desktop/Coder Desktop/Info.plist b/Coder-Desktop/Coder-Desktop/Info.plist
similarity index 100%
rename from Coder Desktop/Coder Desktop/Info.plist
rename to Coder-Desktop/Coder-Desktop/Info.plist
diff --git a/Coder Desktop/Coder Desktop/MenuBarIconController.swift b/Coder-Desktop/Coder-Desktop/MenuBarIconController.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/MenuBarIconController.swift
rename to Coder-Desktop/Coder-Desktop/MenuBarIconController.swift
diff --git a/Coder Desktop/Coder Desktop/Preview Content/Preview Assets.xcassets/Contents.json b/Coder-Desktop/Coder-Desktop/Preview Content/Preview Assets.xcassets/Contents.json
similarity index 100%
rename from Coder Desktop/Coder Desktop/Preview Content/Preview Assets.xcassets/Contents.json
rename to Coder-Desktop/Coder-Desktop/Preview Content/Preview Assets.xcassets/Contents.json
diff --git a/Coder Desktop/Coder Desktop/Preview Content/PreviewVPN.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Preview Content/PreviewVPN.swift
rename to Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift
diff --git a/Coder Desktop/Coder Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/State.swift
rename to Coder-Desktop/Coder-Desktop/State.swift
diff --git a/Coder Desktop/Coder Desktop/Theme.swift b/Coder-Desktop/Coder-Desktop/Theme.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Theme.swift
rename to Coder-Desktop/Coder-Desktop/Theme.swift
diff --git a/Coder Desktop/Coder Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/VPN/MenuState.swift
rename to Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
diff --git a/Coder Desktop/Coder Desktop/VPN/NetworkExtension.swift b/Coder-Desktop/Coder-Desktop/VPN/NetworkExtension.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/VPN/NetworkExtension.swift
rename to Coder-Desktop/Coder-Desktop/VPN/NetworkExtension.swift
diff --git a/Coder Desktop/Coder Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/VPN/VPNService.swift
rename to Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
diff --git a/Coder Desktop/Coder Desktop/VPN/VPNSystemExtension.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/VPN/VPNSystemExtension.swift
rename to Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift
diff --git a/Coder Desktop/Coder Desktop/Views/Agents.swift b/Coder-Desktop/Coder-Desktop/Views/Agents.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/Agents.swift
rename to Coder-Desktop/Coder-Desktop/Views/Agents.swift
diff --git a/Coder Desktop/Coder Desktop/Views/AuthButton.swift b/Coder-Desktop/Coder-Desktop/Views/AuthButton.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/AuthButton.swift
rename to Coder-Desktop/Coder-Desktop/Views/AuthButton.swift
diff --git a/Coder Desktop/Coder Desktop/Views/ButtonRow.swift b/Coder-Desktop/Coder-Desktop/Views/ButtonRow.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/ButtonRow.swift
rename to Coder-Desktop/Coder-Desktop/Views/ButtonRow.swift
diff --git a/Coder Desktop/Coder Desktop/Views/InvalidAgents.swift b/Coder-Desktop/Coder-Desktop/Views/InvalidAgents.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/InvalidAgents.swift
rename to Coder-Desktop/Coder-Desktop/Views/InvalidAgents.swift
diff --git a/Coder Desktop/Coder Desktop/Views/LoginForm.swift b/Coder-Desktop/Coder-Desktop/Views/LoginForm.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/LoginForm.swift
rename to Coder-Desktop/Coder-Desktop/Views/LoginForm.swift
diff --git a/Coder Desktop/Coder Desktop/Views/ResponsiveLink.swift b/Coder-Desktop/Coder-Desktop/Views/ResponsiveLink.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/ResponsiveLink.swift
rename to Coder-Desktop/Coder-Desktop/Views/ResponsiveLink.swift
diff --git a/Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift
rename to Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift
diff --git a/Coder Desktop/Coder Desktop/Views/Settings/LiteralHeaderModal.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeaderModal.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/Settings/LiteralHeaderModal.swift
rename to Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeaderModal.swift
diff --git a/Coder Desktop/Coder Desktop/Views/Settings/LiteralHeadersSection.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeadersSection.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/Settings/LiteralHeadersSection.swift
rename to Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeadersSection.swift
diff --git a/Coder Desktop/Coder Desktop/Views/Settings/NetworkTab.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/NetworkTab.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/Settings/NetworkTab.swift
rename to Coder-Desktop/Coder-Desktop/Views/Settings/NetworkTab.swift
diff --git a/Coder Desktop/Coder Desktop/Views/Settings/Settings.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/Settings.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/Settings/Settings.swift
rename to Coder-Desktop/Coder-Desktop/Views/Settings/Settings.swift
diff --git a/Coder Desktop/Coder Desktop/Views/TrayDivider.swift b/Coder-Desktop/Coder-Desktop/Views/TrayDivider.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/TrayDivider.swift
rename to Coder-Desktop/Coder-Desktop/Views/TrayDivider.swift
diff --git a/Coder Desktop/Coder Desktop/Views/Util.swift b/Coder-Desktop/Coder-Desktop/Views/Util.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/Util.swift
rename to Coder-Desktop/Coder-Desktop/Views/Util.swift
diff --git a/Coder Desktop/Coder Desktop/Views/VPNMenu.swift b/Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/VPNMenu.swift
rename to Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift
diff --git a/Coder Desktop/Coder Desktop/Views/VPNMenuItem.swift b/Coder-Desktop/Coder-Desktop/Views/VPNMenuItem.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/VPNMenuItem.swift
rename to Coder-Desktop/Coder-Desktop/Views/VPNMenuItem.swift
diff --git a/Coder Desktop/Coder Desktop/Views/VPNState.swift b/Coder-Desktop/Coder-Desktop/Views/VPNState.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Views/VPNState.swift
rename to Coder-Desktop/Coder-Desktop/Views/VPNState.swift
diff --git a/Coder Desktop/Coder Desktop/Windows.swift b/Coder-Desktop/Coder-Desktop/Windows.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/Windows.swift
rename to Coder-Desktop/Coder-Desktop/Windows.swift
diff --git a/Coder Desktop/Coder Desktop/XPCInterface.swift b/Coder-Desktop/Coder-Desktop/XPCInterface.swift
similarity index 100%
rename from Coder Desktop/Coder Desktop/XPCInterface.swift
rename to Coder-Desktop/Coder-Desktop/XPCInterface.swift
diff --git a/Coder Desktop/Coder DesktopTests/AgentsTests.swift b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift
similarity index 100%
rename from Coder Desktop/Coder DesktopTests/AgentsTests.swift
rename to Coder-Desktop/Coder-DesktopTests/AgentsTests.swift
diff --git a/Coder Desktop/Coder DesktopTests/LiteralHeadersSettingTests.swift b/Coder-Desktop/Coder-DesktopTests/LiteralHeadersSettingTests.swift
similarity index 100%
rename from Coder Desktop/Coder DesktopTests/LiteralHeadersSettingTests.swift
rename to Coder-Desktop/Coder-DesktopTests/LiteralHeadersSettingTests.swift
diff --git a/Coder Desktop/Coder DesktopTests/LoginFormTests.swift b/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift
similarity index 100%
rename from Coder Desktop/Coder DesktopTests/LoginFormTests.swift
rename to Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift
diff --git a/Coder Desktop/Coder DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
similarity index 100%
rename from Coder Desktop/Coder DesktopTests/Util.swift
rename to Coder-Desktop/Coder-DesktopTests/Util.swift
diff --git a/Coder Desktop/Coder DesktopTests/VPNMenuStateTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
similarity index 100%
rename from Coder Desktop/Coder DesktopTests/VPNMenuStateTests.swift
rename to Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
diff --git a/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift
similarity index 100%
rename from Coder Desktop/Coder DesktopTests/VPNMenuTests.swift
rename to Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift
diff --git a/Coder Desktop/Coder DesktopTests/VPNStateTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNStateTests.swift
similarity index 100%
rename from Coder Desktop/Coder DesktopTests/VPNStateTests.swift
rename to Coder-Desktop/Coder-DesktopTests/VPNStateTests.swift
diff --git a/Coder Desktop/Coder DesktopUITests/Coder_DesktopUITests.swift b/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITests.swift
similarity index 100%
rename from Coder Desktop/Coder DesktopUITests/Coder_DesktopUITests.swift
rename to Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITests.swift
diff --git a/Coder Desktop/Coder DesktopUITests/Coder_DesktopUITestsLaunchTests.swift b/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITestsLaunchTests.swift
similarity index 100%
rename from Coder Desktop/Coder DesktopUITests/Coder_DesktopUITestsLaunchTests.swift
rename to Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITestsLaunchTests.swift
diff --git a/Coder Desktop/CoderSDK/Client.swift b/Coder-Desktop/CoderSDK/Client.swift
similarity index 100%
rename from Coder Desktop/CoderSDK/Client.swift
rename to Coder-Desktop/CoderSDK/Client.swift
diff --git a/Coder Desktop/CoderSDK/CoderSDK.h b/Coder-Desktop/CoderSDK/CoderSDK.h
similarity index 100%
rename from Coder Desktop/CoderSDK/CoderSDK.h
rename to Coder-Desktop/CoderSDK/CoderSDK.h
diff --git a/Coder Desktop/CoderSDK/Date.swift b/Coder-Desktop/CoderSDK/Date.swift
similarity index 100%
rename from Coder Desktop/CoderSDK/Date.swift
rename to Coder-Desktop/CoderSDK/Date.swift
diff --git a/Coder Desktop/CoderSDK/Deployment.swift b/Coder-Desktop/CoderSDK/Deployment.swift
similarity index 100%
rename from Coder Desktop/CoderSDK/Deployment.swift
rename to Coder-Desktop/CoderSDK/Deployment.swift
diff --git a/Coder Desktop/CoderSDK/HTTP.swift b/Coder-Desktop/CoderSDK/HTTP.swift
similarity index 100%
rename from Coder Desktop/CoderSDK/HTTP.swift
rename to Coder-Desktop/CoderSDK/HTTP.swift
diff --git a/Coder Desktop/CoderSDK/User.swift b/Coder-Desktop/CoderSDK/User.swift
similarity index 100%
rename from Coder Desktop/CoderSDK/User.swift
rename to Coder-Desktop/CoderSDK/User.swift
diff --git a/Coder Desktop/CoderSDKTests/CoderSDKTests.swift b/Coder-Desktop/CoderSDKTests/CoderSDKTests.swift
similarity index 100%
rename from Coder Desktop/CoderSDKTests/CoderSDKTests.swift
rename to Coder-Desktop/CoderSDKTests/CoderSDKTests.swift
diff --git a/Coder Desktop/Resources/.mutagenversion b/Coder-Desktop/Resources/.mutagenversion
similarity index 100%
rename from Coder Desktop/Resources/.mutagenversion
rename to Coder-Desktop/Resources/.mutagenversion
diff --git a/Coder Desktop/VPN/Info.plist b/Coder-Desktop/VPN/Info.plist
similarity index 100%
rename from Coder Desktop/VPN/Info.plist
rename to Coder-Desktop/VPN/Info.plist
diff --git a/Coder Desktop/VPN/Manager.swift b/Coder-Desktop/VPN/Manager.swift
similarity index 100%
rename from Coder Desktop/VPN/Manager.swift
rename to Coder-Desktop/VPN/Manager.swift
diff --git a/Coder Desktop/VPN/PacketTunnelProvider.swift b/Coder-Desktop/VPN/PacketTunnelProvider.swift
similarity index 100%
rename from Coder Desktop/VPN/PacketTunnelProvider.swift
rename to Coder-Desktop/VPN/PacketTunnelProvider.swift
diff --git a/Coder Desktop/VPN/TunnelHandle.swift b/Coder-Desktop/VPN/TunnelHandle.swift
similarity index 100%
rename from Coder Desktop/VPN/TunnelHandle.swift
rename to Coder-Desktop/VPN/TunnelHandle.swift
diff --git a/Coder Desktop/VPN/XPCInterface.swift b/Coder-Desktop/VPN/XPCInterface.swift
similarity index 100%
rename from Coder Desktop/VPN/XPCInterface.swift
rename to Coder-Desktop/VPN/XPCInterface.swift
diff --git a/Coder Desktop/VPN/com_coder_Coder_Desktop_VPN-Bridging-Header.h b/Coder-Desktop/VPN/com_coder_Coder_Desktop_VPN-Bridging-Header.h
similarity index 100%
rename from Coder Desktop/VPN/com_coder_Coder_Desktop_VPN-Bridging-Header.h
rename to Coder-Desktop/VPN/com_coder_Coder_Desktop_VPN-Bridging-Header.h
diff --git a/Coder Desktop/VPN/main.swift b/Coder-Desktop/VPN/main.swift
similarity index 100%
rename from Coder Desktop/VPN/main.swift
rename to Coder-Desktop/VPN/main.swift
diff --git a/Coder Desktop/VPNLib/Convert.swift b/Coder-Desktop/VPNLib/Convert.swift
similarity index 100%
rename from Coder Desktop/VPNLib/Convert.swift
rename to Coder-Desktop/VPNLib/Convert.swift
diff --git a/Coder Desktop/VPNLib/Download.swift b/Coder-Desktop/VPNLib/Download.swift
similarity index 100%
rename from Coder Desktop/VPNLib/Download.swift
rename to Coder-Desktop/VPNLib/Download.swift
diff --git a/Coder Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
similarity index 100%
rename from Coder Desktop/VPNLib/FileSync/FileSyncDaemon.swift
rename to Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
diff --git a/Coder Desktop/VPNLib/FileSync/daemon.grpc.swift b/Coder-Desktop/VPNLib/FileSync/daemon.grpc.swift
similarity index 99%
rename from Coder Desktop/VPNLib/FileSync/daemon.grpc.swift
rename to Coder-Desktop/VPNLib/FileSync/daemon.grpc.swift
index 4fbe0789..43d25fb9 100644
--- a/Coder Desktop/VPNLib/FileSync/daemon.grpc.swift	
+++ b/Coder-Desktop/VPNLib/FileSync/daemon.grpc.swift
@@ -3,7 +3,7 @@
 // swift-format-ignore-file
 //
 // Generated by the protocol buffer compiler.
-// Source: Coder Desktop/VPNLib/FileSync/daemon.proto
+// Source: Coder-Desktop/VPNLib/FileSync/daemon.proto
 //
 import GRPC
 import NIO
diff --git a/Coder Desktop/VPNLib/FileSync/daemon.pb.swift b/Coder-Desktop/VPNLib/FileSync/daemon.pb.swift
similarity index 98%
rename from Coder Desktop/VPNLib/FileSync/daemon.pb.swift
rename to Coder-Desktop/VPNLib/FileSync/daemon.pb.swift
index 4ed73c69..047ca500 100644
--- a/Coder Desktop/VPNLib/FileSync/daemon.pb.swift	
+++ b/Coder-Desktop/VPNLib/FileSync/daemon.pb.swift
@@ -3,7 +3,7 @@
 // swiftlint:disable all
 //
 // Generated by the Swift generator plugin for the protocol buffer compiler.
-// Source: Coder Desktop/VPNLib/FileSync/daemon.proto
+// Source: Coder-Desktop/VPNLib/FileSync/daemon.proto
 //
 // For information on using the generated types, please see the documentation:
 //   https://github.com/apple/swift-protobuf/
diff --git a/Coder Desktop/VPNLib/FileSync/daemon.proto b/Coder-Desktop/VPNLib/FileSync/daemon.proto
similarity index 100%
rename from Coder Desktop/VPNLib/FileSync/daemon.proto
rename to Coder-Desktop/VPNLib/FileSync/daemon.proto
diff --git a/Coder Desktop/VPNLib/Receiver.swift b/Coder-Desktop/VPNLib/Receiver.swift
similarity index 100%
rename from Coder Desktop/VPNLib/Receiver.swift
rename to Coder-Desktop/VPNLib/Receiver.swift
diff --git a/Coder Desktop/VPNLib/Sender.swift b/Coder-Desktop/VPNLib/Sender.swift
similarity index 100%
rename from Coder Desktop/VPNLib/Sender.swift
rename to Coder-Desktop/VPNLib/Sender.swift
diff --git a/Coder Desktop/VPNLib/Speaker.swift b/Coder-Desktop/VPNLib/Speaker.swift
similarity index 100%
rename from Coder Desktop/VPNLib/Speaker.swift
rename to Coder-Desktop/VPNLib/Speaker.swift
diff --git a/Coder Desktop/VPNLib/Util.swift b/Coder-Desktop/VPNLib/Util.swift
similarity index 100%
rename from Coder Desktop/VPNLib/Util.swift
rename to Coder-Desktop/VPNLib/Util.swift
diff --git a/Coder Desktop/VPNLib/VPNLib.h b/Coder-Desktop/VPNLib/VPNLib.h
similarity index 100%
rename from Coder Desktop/VPNLib/VPNLib.h
rename to Coder-Desktop/VPNLib/VPNLib.h
diff --git a/Coder Desktop/VPNLib/XPC.swift b/Coder-Desktop/VPNLib/XPC.swift
similarity index 100%
rename from Coder Desktop/VPNLib/XPC.swift
rename to Coder-Desktop/VPNLib/XPC.swift
diff --git a/Coder Desktop/VPNLib/vpn.pb.swift b/Coder-Desktop/VPNLib/vpn.pb.swift
similarity index 99%
rename from Coder Desktop/VPNLib/vpn.pb.swift
rename to Coder-Desktop/VPNLib/vpn.pb.swift
index 0dd7238b..525f55bb 100644
--- a/Coder Desktop/VPNLib/vpn.pb.swift	
+++ b/Coder-Desktop/VPNLib/vpn.pb.swift
@@ -3,7 +3,7 @@
 // swiftlint:disable all
 //
 // Generated by the Swift generator plugin for the protocol buffer compiler.
-// Source: Coder Desktop/VPNLib/vpn.proto
+// Source: Coder-Desktop/VPNLib/vpn.proto
 //
 // For information on using the generated types, please see the documentation:
 //   https://github.com/apple/swift-protobuf/
diff --git a/Coder Desktop/VPNLib/vpn.proto b/Coder-Desktop/VPNLib/vpn.proto
similarity index 100%
rename from Coder Desktop/VPNLib/vpn.proto
rename to Coder-Desktop/VPNLib/vpn.proto
diff --git a/Coder Desktop/VPNLibTests/ConvertTests.swift b/Coder-Desktop/VPNLibTests/ConvertTests.swift
similarity index 100%
rename from Coder Desktop/VPNLibTests/ConvertTests.swift
rename to Coder-Desktop/VPNLibTests/ConvertTests.swift
diff --git a/Coder Desktop/VPNLibTests/DownloadTests.swift b/Coder-Desktop/VPNLibTests/DownloadTests.swift
similarity index 100%
rename from Coder Desktop/VPNLibTests/DownloadTests.swift
rename to Coder-Desktop/VPNLibTests/DownloadTests.swift
diff --git a/Coder Desktop/VPNLibTests/ProtoTests.swift b/Coder-Desktop/VPNLibTests/ProtoTests.swift
similarity index 100%
rename from Coder Desktop/VPNLibTests/ProtoTests.swift
rename to Coder-Desktop/VPNLibTests/ProtoTests.swift
diff --git a/Coder Desktop/VPNLibTests/SpeakerTests.swift b/Coder-Desktop/VPNLibTests/SpeakerTests.swift
similarity index 100%
rename from Coder Desktop/VPNLibTests/SpeakerTests.swift
rename to Coder-Desktop/VPNLibTests/SpeakerTests.swift
diff --git a/Coder Desktop/project.yml b/Coder-Desktop/project.yml
similarity index 96%
rename from Coder Desktop/project.yml
rename to Coder-Desktop/project.yml
index 4b0eef6d..5411b5a4 100644
--- a/Coder Desktop/project.yml	
+++ b/Coder-Desktop/project.yml
@@ -1,4 +1,4 @@
-name: "Coder Desktop"
+name: "Coder-Desktop"
 options:
   bundleIdPrefix: com.coder
   deploymentTarget:
@@ -121,11 +121,11 @@ targets:
     type: application
     platform: macOS
     sources:
-      - path: Coder Desktop
+      - path: Coder-Desktop
       - path: Resources
         buildPhase: resources
     entitlements:
-      path: Coder Desktop/Coder_Desktop.entitlements
+      path: Coder-Desktop/Coder-Desktop.entitlements
       properties:
         com.apple.developer.networking.networkextension:
           - packet-tunnel-provider${PTP_SUFFIX}
@@ -140,7 +140,7 @@ targets:
         CODE_SIGN_IDENTITY: "Apple Development"
         CODE_SIGN_STYLE: Automatic
         COMBINE_HIDPI_IMAGES: YES
-        DEVELOPMENT_ASSET_PATHS: '"Coder Desktop/Preview Content"' # Adds development assets.
+        DEVELOPMENT_ASSET_PATHS: '"Coder-Desktop/Preview Content"' # Adds development assets.
         ENABLE_HARDENED_RUNTIME: YES
         ENABLE_PREVIEWS: YES
         INFOPLIST_KEY_LSUIElement: YES
@@ -174,19 +174,19 @@ targets:
       - package: LaunchAtLogin
     scheme:
       testPlans:
-        - path: Coder Desktop.xctestplan
+        - path: Coder-Desktop.xctestplan
       testTargets:
-        - Coder DesktopTests
-        - Coder DesktopUITests
+        - Coder-DesktopTests
+        - Coder-DesktopUITests
     buildToolPlugins:
       - plugin: SwiftLintBuildToolPlugin
         package: SwiftLintPlugins
 
-  Coder DesktopTests:
+  Coder-DesktopTests:
     type: bundle.unit-test
     platform: macOS
     sources:
-      - path: Coder DesktopTests
+      - path: Coder-DesktopTests
     settings:
       base:
         BUNDLE_LOADER: "$(TEST_HOST)"
@@ -199,11 +199,11 @@ targets:
       - package: ViewInspector
       - package: Mocker
 
-  Coder DesktopUITests:
+  Coder-DesktopUITests:
     type: bundle.ui-testing
     platform: macOS
     sources:
-      - path: Coder DesktopUITests
+      - path: Coder-DesktopUITests
     settings:
       base:
         PRODUCT_BUNDLE_IDENTIFIER: "com.coder.Coder-DesktopUITests"
diff --git a/Makefile b/Makefile
index 259c1ce5..14faf6dd 100644
--- a/Makefile
+++ b/Makefile
@@ -14,9 +14,10 @@ LINTFLAGS :=
 FMTFLAGS :=
 endif
 
-PROJECT := Coder\ Desktop
-XCPROJECT := Coder\ Desktop/Coder\ Desktop.xcodeproj
+PROJECT := Coder-Desktop
+XCPROJECT := Coder-Desktop/Coder-Desktop.xcodeproj
 SCHEME := Coder\ Desktop
+TEST_PLAN := Coder-Desktop
 SWIFT_VERSION := 6.0
 
 MUTAGEN_RESOURCES := mutagen-agents.tar.gz mutagen-darwin-arm64 mutagen-darwin-amd64
@@ -55,7 +56,7 @@ setup: \
 
 # Mutagen resources
 $(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)): $(PROJECT)/Resources/.mutagenversion
-	curl -sL "https://storage.googleapis.com/coder-desktop/mutagen/$(MUTAGEN_VERSION)/$$(basename "$@")" -o "$@"
+	curl -sL "https://storage.googleapis.com/coder-desktop/mutagen/$(MUTAGEN_VERSION)/$(notdir $@)" -o "$@"
 	chmod +x "$@"
 
 $(XCPROJECT): $(PROJECT)/project.yml
@@ -69,13 +70,13 @@ $(XCPROJECT): $(PROJECT)/project.yml
 		xcodegen
 
 $(PROJECT)/VPNLib/vpn.pb.swift: $(PROJECT)/VPNLib/vpn.proto
-	protoc --swift_opt=Visibility=public --swift_out=. 'Coder Desktop/VPNLib/vpn.proto'
+	protoc --swift_opt=Visibility=public --swift_out=. 'Coder-Desktop/VPNLib/vpn.proto'
 
 $(PROJECT)/VPNLib/FileSync/daemon.pb.swift: $(PROJECT)/VPNLib/FileSync/daemon.proto
 	protoc \
 		--swift_out=.\
 		--grpc-swift_out=. \
-		'Coder Desktop/VPNLib/FileSync/daemon.proto'
+		'Coder-Desktop/VPNLib/FileSync/daemon.proto'
 
 $(KEYCHAIN_FILE):
 	security create-keychain -p "" "$(APP_SIGNING_KEYCHAIN)"
@@ -115,7 +116,7 @@ test: $(XCPROJECT) ## Run all tests
 	set -o pipefail && xcodebuild test \
 		-project $(XCPROJECT) \
 		-scheme $(SCHEME) \
-		-testPlan $(SCHEME) \
+		-testPlan $(TEST_PLAN) \
 		-skipPackagePluginValidation \
 		CODE_SIGNING_REQUIRED=NO \
 		CODE_SIGNING_ALLOWED=NO | xcbeautify
@@ -173,6 +174,6 @@ help: ## Show this help
 
 .PHONY: watch-gen
 watch-gen: ## Generate Xcode project file and watch for changes
-	watchexec -w 'Coder Desktop/project.yml' make $(XCPROJECT)
+	watchexec -w 'Coder-Desktop/project.yml' make $(XCPROJECT)
 
 print-%: ; @echo $*=$($*)
diff --git a/scripts/build.sh b/scripts/build.sh
index 3be1045a..b1351da1 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -116,11 +116,11 @@ mkdir -p "$out"
 mkdir build
 
 # Archive the app
-ARCHIVE_PATH="./build/Coder Desktop.xcarchive"
+ARCHIVE_PATH="./build/Coder-Desktop.xcarchive"
 mkdir -p build
 
 xcodebuild \
-  -project "Coder Desktop/Coder Desktop.xcodeproj" \
+  -project "Coder-Desktop/Coder-Desktop.xcodeproj" \
   -scheme "Coder Desktop" \
   -configuration "Release" \
   -archivePath "$ARCHIVE_PATH" \
@@ -165,7 +165,7 @@ xcodebuild \
   -exportPath "$EXPORT_PATH"
 
 BUILT_APP_PATH="$EXPORT_PATH/Coder Desktop.app"
-PKG_PATH="$out/CoderDesktop.pkg"
+PKG_PATH="$out/Coder-Desktop.pkg"
 DSYM_ZIPPED_PATH="$out/coder-desktop-dsyms.zip"
 APP_ZIPPED_PATH="$out/coder-desktop-universal.zip"
 
diff --git a/scripts/update-cask.sh b/scripts/update-cask.sh
index c9a71a54..4277184a 100755
--- a/scripts/update-cask.sh
+++ b/scripts/update-cask.sh
@@ -44,15 +44,15 @@ done
   exit 1
 }
 
-# Download the CoderDesktop pkg
+# Download the Coder-Desktop pkg
 GH_RELEASE_FOLDER=$(mktemp -d)
 
 gh release download "$VERSION" \
   --repo coder/coder-desktop-macos \
   --dir "$GH_RELEASE_FOLDER" \
-  --pattern 'CoderDesktop.pkg'
+  --pattern 'Coder-Desktop.pkg'
 
-HASH=$(shasum -a 256 "$GH_RELEASE_FOLDER"/CoderDesktop.pkg | awk '{print $1}' | tr -d '\n')
+HASH=$(shasum -a 256 "$GH_RELEASE_FOLDER"/Coder-Desktop.pkg | awk '{print $1}' | tr -d '\n')
 
 IS_PREVIEW=false
 if [[ "$VERSION" == "preview" ]]; then
@@ -97,7 +97,7 @@ cask "coder-desktop${SUFFIX}" do
   version "${VERSION#v}"
   sha256 $([ "$IS_PREVIEW" = true ] && echo ":no_check" || echo "\"${HASH}\"")
 
-  url "https://github.com/coder/coder-desktop-macos/releases/download/$([ "$IS_PREVIEW" = true ] && echo "${TAG}" || echo "v#{version}")/CoderDesktop.pkg"
+  url "https://github.com/coder/coder-desktop-macos/releases/download/$([ "$IS_PREVIEW" = true ] && echo "${TAG}" || echo "v#{version}")/Coder-Desktop.pkg"
   name "Coder Desktop"
   desc "Native desktop client for Coder"
   homepage "https://github.com/coder/coder-desktop-macos"
@@ -105,7 +105,7 @@ cask "coder-desktop${SUFFIX}" do
   conflicts_with cask: "coder/coder/${CONFLICTS_WITH}"
   depends_on macos: ">= :sonoma"
 
-  pkg "CoderDesktop.pkg"
+  pkg "Coder-Desktop.pkg"
 
   uninstall quit:       [
               "com.coder.Coder-Desktop",

From 6947811ae51e91cc44028bc381448737ff4ad137 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Tue, 18 Mar 2025 13:33:33 +1100
Subject: [PATCH 06/25] chore(pkgbuild): delete existing app during preinstall,
 `spctl --assess` during postinstall (#112)

Relates to #83.

It looks like deleting the app does indeed kill the NE process, so we should do that during `preinstall`.

In case the XPC issue we've been seeing is due to a race between the app being opened, and Gatekeeper ingesting the notarization ticket, we'll also force Gatekeeper to read the ticket by running `spctl -a` on the app bundle, and the extension bundle. The latter always fails, but an attempt to read it can't hurt.
---
 pkgbuild/scripts/postinstall | 8 ++++++++
 pkgbuild/scripts/preinstall  | 7 +++++++
 2 files changed, 15 insertions(+)

diff --git a/pkgbuild/scripts/postinstall b/pkgbuild/scripts/postinstall
index b7dd1bd3..8018af9c 100755
--- a/pkgbuild/scripts/postinstall
+++ b/pkgbuild/scripts/postinstall
@@ -3,6 +3,14 @@
 RUNNING_MARKER_FILE="/tmp/coder_desktop_running"
 VPN_MARKER_FILE="/tmp/coder_vpn_was_running"
 
+# Before this script, or the user, opens the app, make sure
+# Gatekeeper has ingested the notarization ticket.
+spctl -avvv "/Applications/Coder Desktop.app"
+# spctl can't assess non-apps, so this will always return a non-zero exit code, 
+# but the error message implies at minimum the signature of the extension was 
+# checked. 
+spctl -avvv "/Applications/Coder Desktop.app/Contents/Library/SystemExtensions/com.coder.Coder-Desktop.VPN.systemextension" || true
+
 # Restart Coder Desktop if it was running before
 if [ -f "$RUNNING_MARKER_FILE" ]; then
   echo "Starting Coder Desktop..."
diff --git a/pkgbuild/scripts/preinstall b/pkgbuild/scripts/preinstall
index 66c54e92..83271f3c 100755
--- a/pkgbuild/scripts/preinstall
+++ b/pkgbuild/scripts/preinstall
@@ -35,4 +35,11 @@ echo "Asking com.coder.Coder-Desktop to quit..."
 osascript -e 'if app id "com.coder.Coder-Desktop" is running then' -e 'quit app id "com.coder.Coder-Desktop"' -e 'end if'
 echo "Done."
 
+APP="/Applications/Coder Desktop.app"
+if [ -d "$APP" ]; then
+  echo "Deleting Coder Desktop..."
+  rm -rf "$APP"
+  echo "Done."
+fi
+
 exit 0

From de63a7af518a7b57643bdd53f5c0aed269f5d6a8 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Tue, 18 Mar 2025 14:35:26 +1100
Subject: [PATCH 07/25] feat: add start vpn on launch setting (#108)

Relates to #104.

On-demand is a pretty big lift - in the meantime we'll add a 'start VPN on launch' config setting.
---
 Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift       | 3 +++
 Coder-Desktop/Coder-Desktop/State.swift                  | 8 ++++++++
 .../Coder-Desktop/Views/Settings/GeneralTab.swift        | 9 +++++++--
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index 1d379e91..23b31a2a 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -58,6 +58,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
             if await !vpn.loadNetworkExtensionConfig() {
                 state.reconfigure()
             }
+            if state.startVPNOnLaunch {
+                await vpn.start()
+            }
         }
         // TODO: Start the daemon only once a file sync is configured
         Task {
diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift
index 3e723c9f..fd6182e5 100644
--- a/Coder-Desktop/Coder-Desktop/State.swift
+++ b/Coder-Desktop/Coder-Desktop/State.swift
@@ -54,6 +54,13 @@ class AppState: ObservableObject {
         }
     }
 
+    @Published var startVPNOnLaunch: Bool = UserDefaults.standard.bool(forKey: Keys.startVPNOnLaunch) {
+        didSet {
+            guard persistent else { return }
+            UserDefaults.standard.set(startVPNOnLaunch, forKey: Keys.startVPNOnLaunch)
+        }
+    }
+
     func tunnelProviderProtocol() -> NETunnelProviderProtocol? {
         if !hasSession { return nil }
         let proto = NETunnelProviderProtocol()
@@ -133,6 +140,7 @@ class AppState: ObservableObject {
         static let useLiteralHeaders = "UseLiteralHeaders"
         static let literalHeaders = "LiteralHeaders"
         static let stopVPNOnQuit = "StopVPNOnQuit"
+        static let startVPNOnLaunch = "StartVPNOnLaunch"
     }
 }
 
diff --git a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift
index 27aecabb..532d0f00 100644
--- a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift
@@ -6,11 +6,16 @@ struct GeneralTab: View {
     var body: some View {
         Form {
             Section {
-                LaunchAtLogin.Toggle("Launch at Login")
+                LaunchAtLogin.Toggle("Launch at login")
             }
             Section {
                 Toggle(isOn: $state.stopVPNOnQuit) {
-                    Text("Stop Coder Connect on Quit")
+                    Text("Stop Coder Connect on quit")
+                }
+            }
+            Section {
+                Toggle(isOn: $state.startVPNOnLaunch) {
+                    Text("Start Coder Connect on launch")
                 }
             }
         }.formStyle(.grouped)

From 9d93c0f20c5f0a0ac39b09d30fae8690d5f435e0 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Tue, 18 Mar 2025 14:36:41 +1100
Subject: [PATCH 08/25] chore: sign user out if token is expired (#109)

Closes #107.

When the menu bar icon is clicked, and the user is signed in, and the VPN is disabled, the app will check if the token is expired. If it is, the user will be signed out.

We could have checked this when the VPN is enabled, but the UX seemed worse, and the implementation would have been messy. We would have needed to sign the user out and show an error. Instead, we'll check for expiry in a scenario where the next user step would likely be an interaction that requires a session.
This approach also future-proofs for when functionality becomes usable without the VPN.
---
 .../Coder-Desktop/Coder_DesktopApp.swift      | 20 ++++++++++++----
 Coder-Desktop/Coder-Desktop/State.swift       | 23 +++++++++++++++++++
 Coder-Desktop/CoderSDK/Client.swift           |  8 +++----
 Coder-Desktop/project.yml                     |  6 +++--
 4 files changed, 46 insertions(+), 11 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index 23b31a2a..a8d0c946 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -40,11 +40,21 @@ class AppDelegate: NSObject, NSApplicationDelegate {
     }
 
     func applicationDidFinishLaunching(_: Notification) {
-        menuBar = .init(menuBarExtra: FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
-            VPNMenu<CoderVPNService>().frame(width: 256)
-                .environmentObject(self.vpn)
-                .environmentObject(self.state)
-        })
+        menuBar = .init(menuBarExtra: FluidMenuBarExtra(
+            title: "Coder Desktop",
+            image: "MenuBarIcon",
+            onAppear: {
+                // If the VPN is enabled, it's likely the token isn't expired
+                guard case .disabled = self.vpn.state, self.state.hasSession else { return }
+                Task { @MainActor in
+                    await self.state.handleTokenExpiry()
+                }
+            }, content: {
+                VPNMenu<CoderVPNService>().frame(width: 256)
+                    .environmentObject(self.vpn)
+                    .environmentObject(self.state)
+            }
+        ))
         // Subscribe to system VPN updates
         NotificationCenter.default.addObserver(
             self,
diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift
index fd6182e5..39389540 100644
--- a/Coder-Desktop/Coder-Desktop/State.swift
+++ b/Coder-Desktop/Coder-Desktop/State.swift
@@ -2,10 +2,12 @@ import CoderSDK
 import Foundation
 import KeychainAccess
 import NetworkExtension
+import os
 import SwiftUI
 
 @MainActor
 class AppState: ObservableObject {
+    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppState")
     let appId = Bundle.main.bundleIdentifier!
 
     // Stored in UserDefaults
@@ -102,6 +104,9 @@ class AppState: ObservableObject {
         )
         if hasSession {
             _sessionToken = Published(initialValue: keychainGet(for: Keys.sessionToken))
+            if sessionToken == nil || sessionToken!.isEmpty == true {
+                clearSession()
+            }
         }
     }
 
@@ -112,6 +117,24 @@ class AppState: ObservableObject {
         reconfigure()
     }
 
+    public func handleTokenExpiry() async {
+        if hasSession {
+            let client = Client(url: baseAccessURL!, token: sessionToken!)
+            do {
+                _ = try await client.user("me")
+            } catch let ClientError.api(apiErr) {
+                // Expired token
+                if apiErr.statusCode == 401 {
+                    clearSession()
+                }
+            } catch {
+                // Some other failure, we'll show an error if they try and do something
+                logger.error("failed to check token validity: \(error)")
+                return
+            }
+        }
+    }
+
     public func clearSession() {
         hasSession = false
         sessionToken = nil
diff --git a/Coder-Desktop/CoderSDK/Client.swift b/Coder-Desktop/CoderSDK/Client.swift
index 85bc8f3c..239db14a 100644
--- a/Coder-Desktop/CoderSDK/Client.swift
+++ b/Coder-Desktop/CoderSDK/Client.swift
@@ -104,10 +104,10 @@ public struct Client {
 }
 
 public struct APIError: Decodable, Sendable {
-    let response: Response
-    let statusCode: Int
-    let method: String
-    let url: URL
+    public let response: Response
+    public let statusCode: Int
+    public let method: String
+    public let url: URL
 
     var description: String {
         var components = ["\(method) \(url.absoluteString)\nUnexpected status code \(statusCode):\n\(response.message)"]
diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml
index 5411b5a4..c3c53f99 100644
--- a/Coder-Desktop/project.yml
+++ b/Coder-Desktop/project.yml
@@ -92,10 +92,12 @@ packages:
     url: https://github.com/SimplyDanny/SwiftLintPlugins
     from: 0.57.1
   FluidMenuBarExtra:
-    # Forked so we can dynamically update the menu bar icon.
+    # Forked to:
+    # - Dynamically update the menu bar icon
+    # - Set onAppear/disappear handlers.
     # The upstream repo has a purposefully limited API
     url: https://github.com/coder/fluid-menu-bar-extra
-    revision: 020be37
+    revision: 96a861a
   KeychainAccess:
     url: https://github.com/kishikawakatsumi/KeychainAccess
     branch: e0c7eebc5a4465a3c4680764f26b7a61f567cdaf

From 0550ad11087dd14d18c4992f93225e876ff78dcf Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Tue, 18 Mar 2025 14:40:47 +1100
Subject: [PATCH 09/25] chore: add mutagen gRPC client (#111)

Closes https://github.com/coder/internal/issues/379/

Much like in https://github.com/coder/coder-desktop-windows/pull/48, this PR adds `mutagen-proto.sh` which finds all required files, following `import`s.

Right now, we use this client to stop the daemon over gRPC.
---
 .gitattributes                                |    4 +-
 .../filesystem_behavior_probe_mode.pb.swift   |  109 ++
 .../filesystem_behavior_probe_mode.proto      |   49 +
 .../MutagenSDK/selection_selection.pb.swift   |  119 ++
 .../MutagenSDK/selection_selection.proto      |   46 +
 .../service_daemon_daemon.grpc.swift}         |  100 +-
 .../MutagenSDK/service_daemon_daemon.pb.swift |  208 ++++
 .../MutagenSDK/service_daemon_daemon.proto    |   52 +
 ...synchronization_synchronization.grpc.swift |  908 +++++++++++++++
 ...e_synchronization_synchronization.pb.swift | 1006 +++++++++++++++++
 ...vice_synchronization_synchronization.proto |  168 +++
 ...hronization_compression_algorithm.pb.swift |  113 ++
 ...ynchronization_compression_algorithm.proto |   48 +
 .../synchronization_configuration.pb.swift    |  433 +++++++
 .../synchronization_configuration.proto       |  174 +++
 .../synchronization_core_change.pb.swift      |  140 +++
 .../synchronization_core_change.proto         |   48 +
 .../synchronization_core_conflict.pb.swift    |  123 ++
 .../synchronization_core_conflict.proto       |   52 +
 .../synchronization_core_entry.pb.swift       |  245 ++++
 .../synchronization_core_entry.proto          |  109 ++
 ...ation_core_ignore_ignore_vcs_mode.pb.swift |  106 ++
 ...nization_core_ignore_ignore_vcs_mode.proto |   46 +
 ...ynchronization_core_ignore_syntax.pb.swift |  106 ++
 .../synchronization_core_ignore_syntax.proto  |   46 +
 .../synchronization_core_mode.pb.swift        |  135 +++
 .../synchronization_core_mode.proto           |   69 ++
 ...hronization_core_permissions_mode.pb.swift |  110 ++
 ...ynchronization_core_permissions_mode.proto |   50 +
 .../synchronization_core_problem.pb.swift     |  109 ++
 .../synchronization_core_problem.proto        |   43 +
 ...onization_core_symbolic_link_mode.pb.swift |  118 ++
 ...chronization_core_symbolic_link_mode.proto |   53 +
 ...synchronization_hashing_algorithm.pb.swift |  111 ++
 .../synchronization_hashing_algorithm.proto   |   46 +
 .../synchronization_rsync_receive.pb.swift    |  145 +++
 .../synchronization_rsync_receive.proto       |   56 +
 .../synchronization_scan_mode.pb.swift        |  106 ++
 .../synchronization_scan_mode.proto           |   46 +
 .../synchronization_session.pb.swift          |  370 ++++++
 .../MutagenSDK/synchronization_session.proto  |  100 ++
 .../synchronization_stage_mode.pb.swift       |  115 ++
 .../synchronization_stage_mode.proto          |   50 +
 .../MutagenSDK/synchronization_state.pb.swift |  579 ++++++++++
 .../MutagenSDK/synchronization_state.proto    |  159 +++
 .../synchronization_version.pb.swift          |   98 ++
 .../MutagenSDK/synchronization_version.proto  |   43 +
 .../synchronization_watch_mode.pb.swift       |  118 ++
 .../synchronization_watch_mode.proto          |   53 +
 .../FileSync/MutagenSDK/url_url.pb.swift      |  266 +++++
 .../VPNLib/FileSync/MutagenSDK/url_url.proto  |   90 ++
 Coder-Desktop/VPNLib/FileSync/daemon.pb.swift |   83 --
 Coder-Desktop/VPNLib/FileSync/daemon.proto    |   11 -
 Makefile                                      |   16 +-
 scripts/mutagen-proto.sh                      |  142 +++
 55 files changed, 7946 insertions(+), 102 deletions(-)
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/filesystem_behavior_probe_mode.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/filesystem_behavior_probe_mode.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/selection_selection.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/selection_selection.proto
 rename Coder-Desktop/VPNLib/FileSync/{daemon.grpc.swift => MutagenSDK/service_daemon_daemon.grpc.swift} (73%)
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.grpc.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_compression_algorithm.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_compression_algorithm.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_configuration.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_configuration.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_change.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_change.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_conflict.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_conflict.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_entry.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_entry.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_ignore_vcs_mode.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_ignore_vcs_mode.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_syntax.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_syntax.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_mode.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_mode.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_permissions_mode.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_permissions_mode.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_problem.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_problem.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_symbolic_link_mode.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_symbolic_link_mode.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_hashing_algorithm.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_hashing_algorithm.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_rsync_receive.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_rsync_receive.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_scan_mode.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_scan_mode.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_session.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_session.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_stage_mode.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_stage_mode.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_state.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_state.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_version.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_version.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_watch_mode.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_watch_mode.proto
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/url_url.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/url_url.proto
 delete mode 100644 Coder-Desktop/VPNLib/FileSync/daemon.pb.swift
 delete mode 100644 Coder-Desktop/VPNLib/FileSync/daemon.proto
 create mode 100755 scripts/mutagen-proto.sh

diff --git a/.gitattributes b/.gitattributes
index effdf65f..a0561475 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,3 @@
-nix/create-dmg/package-lock.json -diff
\ No newline at end of file
+**/*.pb.swift linguist-generated=true
+**/*.grpc.swift linguist-generated=true
+Coder-Desktop/VPNLib/FileSync/MutagenSDK/*.proto linguist-generated=true
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/filesystem_behavior_probe_mode.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/filesystem_behavior_probe_mode.pb.swift
new file mode 100644
index 00000000..d82f9055
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/filesystem_behavior_probe_mode.pb.swift
@@ -0,0 +1,109 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: filesystem_behavior_probe_mode.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/filesystem/behavior/probe_mode.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// ProbeMode specifies the mode for filesystem probing.
+enum Behavior_ProbeMode: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// ProbeMode_ProbeModeDefault represents an unspecified probe mode. It
+  /// should be converted to one of the following values based on the desired
+  /// default behavior.
+  case `default` // = 0
+
+  /// ProbeMode_ProbeModeProbe specifies that filesystem behavior should be
+  /// determined using temporary files or, if possible, a "fast-path" mechanism
+  /// (such as filesystem format detection) that provides quick but certain
+  /// determination of filesystem behavior.
+  case probe // = 1
+
+  /// ProbeMode_ProbeModeAssume specifies that filesystem behavior should be
+  /// assumed based on the underlying platform. This is not as accurate as
+  /// ProbeMode_ProbeModeProbe.
+  case assume // = 2
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .probe
+    case 2: self = .assume
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .probe: return 1
+    case .assume: return 2
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Behavior_ProbeMode] = [
+    .default,
+    .probe,
+    .assume,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Behavior_ProbeMode: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "ProbeModeDefault"),
+    1: .same(proto: "ProbeModeProbe"),
+    2: .same(proto: "ProbeModeAssume"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/filesystem_behavior_probe_mode.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/filesystem_behavior_probe_mode.proto
new file mode 100644
index 00000000..c2fb72a6
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/filesystem_behavior_probe_mode.proto
@@ -0,0 +1,49 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/filesystem/behavior/probe_mode.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package behavior;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/filesystem/behavior";
+
+// ProbeMode specifies the mode for filesystem probing.
+enum ProbeMode {
+    // ProbeMode_ProbeModeDefault represents an unspecified probe mode. It
+    // should be converted to one of the following values based on the desired
+    // default behavior.
+    ProbeModeDefault = 0;
+    // ProbeMode_ProbeModeProbe specifies that filesystem behavior should be
+    // determined using temporary files or, if possible, a "fast-path" mechanism
+    // (such as filesystem format detection) that provides quick but certain
+    // determination of filesystem behavior.
+    ProbeModeProbe = 1;
+    // ProbeMode_ProbeModeAssume specifies that filesystem behavior should be
+    // assumed based on the underlying platform. This is not as accurate as
+    // ProbeMode_ProbeModeProbe.
+    ProbeModeAssume = 2;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/selection_selection.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/selection_selection.pb.swift
new file mode 100644
index 00000000..9ea8215d
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/selection_selection.pb.swift
@@ -0,0 +1,119 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: selection_selection.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/selection/selection.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Selection encodes a selection mechanism that can be used to select a
+/// collection of sessions. It should have exactly one member set.
+struct Selection_Selection: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// All, if true, indicates that all sessions should be selected.
+  var all: Bool = false
+
+  /// Specifications is a list of session specifications. Each element may be
+  /// either a session identifier or name (or a prefix thereof). If non-empty,
+  /// it indicates that these specifications should be used to select sessions.
+  var specifications: [String] = []
+
+  /// LabelSelector is a label selector specification. If present (non-empty),
+  /// it indicates that this selector should be used to select sessions.
+  var labelSelector: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "selection"
+
+extension Selection_Selection: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".Selection"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "all"),
+    2: .same(proto: "specifications"),
+    3: .same(proto: "labelSelector"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularBoolField(value: &self.all) }()
+      case 2: try { try decoder.decodeRepeatedStringField(value: &self.specifications) }()
+      case 3: try { try decoder.decodeSingularStringField(value: &self.labelSelector) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if self.all != false {
+      try visitor.visitSingularBoolField(value: self.all, fieldNumber: 1)
+    }
+    if !self.specifications.isEmpty {
+      try visitor.visitRepeatedStringField(value: self.specifications, fieldNumber: 2)
+    }
+    if !self.labelSelector.isEmpty {
+      try visitor.visitSingularStringField(value: self.labelSelector, fieldNumber: 3)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Selection_Selection, rhs: Selection_Selection) -> Bool {
+    if lhs.all != rhs.all {return false}
+    if lhs.specifications != rhs.specifications {return false}
+    if lhs.labelSelector != rhs.labelSelector {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/selection_selection.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/selection_selection.proto
new file mode 100644
index 00000000..552a013e
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/selection_selection.proto
@@ -0,0 +1,46 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/selection/selection.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package selection;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/selection";
+
+// Selection encodes a selection mechanism that can be used to select a
+// collection of sessions. It should have exactly one member set.
+message Selection {
+    // All, if true, indicates that all sessions should be selected.
+    bool all = 1;
+    // Specifications is a list of session specifications. Each element may be
+    // either a session identifier or name (or a prefix thereof). If non-empty,
+    // it indicates that these specifications should be used to select sessions.
+    repeated string specifications = 2;
+    // LabelSelector is a label selector specification. If present (non-empty),
+    // it indicates that this selector should be used to select sessions.
+    string labelSelector = 3;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/daemon.grpc.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.grpc.swift
similarity index 73%
rename from Coder-Desktop/VPNLib/FileSync/daemon.grpc.swift
rename to Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.grpc.swift
index 43d25fb9..809b5c2e 100644
--- a/Coder-Desktop/VPNLib/FileSync/daemon.grpc.swift
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.grpc.swift
@@ -3,7 +3,7 @@
 // swift-format-ignore-file
 //
 // Generated by the protocol buffer compiler.
-// Source: Coder-Desktop/VPNLib/FileSync/daemon.proto
+// Source: service_daemon_daemon.proto
 //
 import GRPC
 import NIO
@@ -16,6 +16,11 @@ internal protocol Daemon_DaemonClientProtocol: GRPCClient {
   var serviceName: String { get }
   var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? { get }
 
+  func version(
+    _ request: Daemon_VersionRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Daemon_VersionRequest, Daemon_VersionResponse>
+
   func terminate(
     _ request: Daemon_TerminateRequest,
     callOptions: CallOptions?
@@ -27,6 +32,24 @@ extension Daemon_DaemonClientProtocol {
     return "daemon.Daemon"
   }
 
+  /// Unary call to Version
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to Version.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func version(
+    _ request: Daemon_VersionRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Daemon_VersionRequest, Daemon_VersionResponse> {
+    return self.makeUnaryCall(
+      path: Daemon_DaemonClientMetadata.Methods.version.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeVersionInterceptors() ?? []
+    )
+  }
+
   /// Unary call to Terminate
   ///
   /// - Parameters:
@@ -108,6 +131,11 @@ internal protocol Daemon_DaemonAsyncClientProtocol: GRPCClient {
   static var serviceDescriptor: GRPCServiceDescriptor { get }
   var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? { get }
 
+  func makeVersionCall(
+    _ request: Daemon_VersionRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Daemon_VersionRequest, Daemon_VersionResponse>
+
   func makeTerminateCall(
     _ request: Daemon_TerminateRequest,
     callOptions: CallOptions?
@@ -124,6 +152,18 @@ extension Daemon_DaemonAsyncClientProtocol {
     return nil
   }
 
+  internal func makeVersionCall(
+    _ request: Daemon_VersionRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Daemon_VersionRequest, Daemon_VersionResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Daemon_DaemonClientMetadata.Methods.version.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeVersionInterceptors() ?? []
+    )
+  }
+
   internal func makeTerminateCall(
     _ request: Daemon_TerminateRequest,
     callOptions: CallOptions? = nil
@@ -139,6 +179,18 @@ extension Daemon_DaemonAsyncClientProtocol {
 
 @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
 extension Daemon_DaemonAsyncClientProtocol {
+  internal func version(
+    _ request: Daemon_VersionRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Daemon_VersionResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Daemon_DaemonClientMetadata.Methods.version.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeVersionInterceptors() ?? []
+    )
+  }
+
   internal func terminate(
     _ request: Daemon_TerminateRequest,
     callOptions: CallOptions? = nil
@@ -171,6 +223,9 @@ internal struct Daemon_DaemonAsyncClient: Daemon_DaemonAsyncClientProtocol {
 
 internal protocol Daemon_DaemonClientInterceptorFactoryProtocol: Sendable {
 
+  /// - Returns: Interceptors to use when invoking 'version'.
+  func makeVersionInterceptors() -> [ClientInterceptor<Daemon_VersionRequest, Daemon_VersionResponse>]
+
   /// - Returns: Interceptors to use when invoking 'terminate'.
   func makeTerminateInterceptors() -> [ClientInterceptor<Daemon_TerminateRequest, Daemon_TerminateResponse>]
 }
@@ -180,11 +235,18 @@ internal enum Daemon_DaemonClientMetadata {
     name: "Daemon",
     fullName: "daemon.Daemon",
     methods: [
+      Daemon_DaemonClientMetadata.Methods.version,
       Daemon_DaemonClientMetadata.Methods.terminate,
     ]
   )
 
   internal enum Methods {
+    internal static let version = GRPCMethodDescriptor(
+      name: "Version",
+      path: "/daemon.Daemon/Version",
+      type: GRPCCallType.unary
+    )
+
     internal static let terminate = GRPCMethodDescriptor(
       name: "Terminate",
       path: "/daemon.Daemon/Terminate",
@@ -197,6 +259,8 @@ internal enum Daemon_DaemonClientMetadata {
 internal protocol Daemon_DaemonProvider: CallHandlerProvider {
   var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? { get }
 
+  func version(request: Daemon_VersionRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Daemon_VersionResponse>
+
   func terminate(request: Daemon_TerminateRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Daemon_TerminateResponse>
 }
 
@@ -212,6 +276,15 @@ extension Daemon_DaemonProvider {
     context: CallHandlerContext
   ) -> GRPCServerHandlerProtocol? {
     switch name {
+    case "Version":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Daemon_VersionRequest>(),
+        responseSerializer: ProtobufSerializer<Daemon_VersionResponse>(),
+        interceptors: self.interceptors?.makeVersionInterceptors() ?? [],
+        userFunction: self.version(request:context:)
+      )
+
     case "Terminate":
       return UnaryServerHandler(
         context: context,
@@ -233,6 +306,11 @@ internal protocol Daemon_DaemonAsyncProvider: CallHandlerProvider, Sendable {
   static var serviceDescriptor: GRPCServiceDescriptor { get }
   var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? { get }
 
+  func version(
+    request: Daemon_VersionRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Daemon_VersionResponse
+
   func terminate(
     request: Daemon_TerminateRequest,
     context: GRPCAsyncServerCallContext
@@ -258,6 +336,15 @@ extension Daemon_DaemonAsyncProvider {
     context: CallHandlerContext
   ) -> GRPCServerHandlerProtocol? {
     switch name {
+    case "Version":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Daemon_VersionRequest>(),
+        responseSerializer: ProtobufSerializer<Daemon_VersionResponse>(),
+        interceptors: self.interceptors?.makeVersionInterceptors() ?? [],
+        wrapping: { try await self.version(request: $0, context: $1) }
+      )
+
     case "Terminate":
       return GRPCAsyncServerHandler(
         context: context,
@@ -275,6 +362,10 @@ extension Daemon_DaemonAsyncProvider {
 
 internal protocol Daemon_DaemonServerInterceptorFactoryProtocol: Sendable {
 
+  /// - Returns: Interceptors to use when handling 'version'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makeVersionInterceptors() -> [ServerInterceptor<Daemon_VersionRequest, Daemon_VersionResponse>]
+
   /// - Returns: Interceptors to use when handling 'terminate'.
   ///   Defaults to calling `self.makeInterceptors()`.
   func makeTerminateInterceptors() -> [ServerInterceptor<Daemon_TerminateRequest, Daemon_TerminateResponse>]
@@ -285,11 +376,18 @@ internal enum Daemon_DaemonServerMetadata {
     name: "Daemon",
     fullName: "daemon.Daemon",
     methods: [
+      Daemon_DaemonServerMetadata.Methods.version,
       Daemon_DaemonServerMetadata.Methods.terminate,
     ]
   )
 
   internal enum Methods {
+    internal static let version = GRPCMethodDescriptor(
+      name: "Version",
+      path: "/daemon.Daemon/Version",
+      type: GRPCCallType.unary
+    )
+
     internal static let terminate = GRPCMethodDescriptor(
       name: "Terminate",
       path: "/daemon.Daemon/Terminate",
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.pb.swift
new file mode 100644
index 00000000..f00093a2
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.pb.swift
@@ -0,0 +1,208 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: service_daemon_daemon.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/service/daemon/daemon.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+struct Daemon_VersionRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+struct Daemon_VersionResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// TODO: Should we encapsulate these inside a Version message type, perhaps
+  /// in the mutagen package?
+  var major: UInt64 = 0
+
+  var minor: UInt64 = 0
+
+  var patch: UInt64 = 0
+
+  var tag: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+struct Daemon_TerminateRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+struct Daemon_TerminateResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "daemon"
+
+extension Daemon_VersionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".VersionRequest"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Daemon_VersionRequest, rhs: Daemon_VersionRequest) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Daemon_VersionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".VersionResponse"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "major"),
+    2: .same(proto: "minor"),
+    3: .same(proto: "patch"),
+    4: .same(proto: "tag"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularUInt64Field(value: &self.major) }()
+      case 2: try { try decoder.decodeSingularUInt64Field(value: &self.minor) }()
+      case 3: try { try decoder.decodeSingularUInt64Field(value: &self.patch) }()
+      case 4: try { try decoder.decodeSingularStringField(value: &self.tag) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if self.major != 0 {
+      try visitor.visitSingularUInt64Field(value: self.major, fieldNumber: 1)
+    }
+    if self.minor != 0 {
+      try visitor.visitSingularUInt64Field(value: self.minor, fieldNumber: 2)
+    }
+    if self.patch != 0 {
+      try visitor.visitSingularUInt64Field(value: self.patch, fieldNumber: 3)
+    }
+    if !self.tag.isEmpty {
+      try visitor.visitSingularStringField(value: self.tag, fieldNumber: 4)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Daemon_VersionResponse, rhs: Daemon_VersionResponse) -> Bool {
+    if lhs.major != rhs.major {return false}
+    if lhs.minor != rhs.minor {return false}
+    if lhs.patch != rhs.patch {return false}
+    if lhs.tag != rhs.tag {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Daemon_TerminateRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".TerminateRequest"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Daemon_TerminateRequest, rhs: Daemon_TerminateRequest) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Daemon_TerminateResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".TerminateResponse"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Daemon_TerminateResponse, rhs: Daemon_TerminateResponse) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.proto
new file mode 100644
index 00000000..c6604cf9
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_daemon_daemon.proto
@@ -0,0 +1,52 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/service/daemon/daemon.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package daemon;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/service/daemon";
+
+message VersionRequest{}
+
+message VersionResponse {
+    // TODO: Should we encapsulate these inside a Version message type, perhaps
+    // in the mutagen package?
+    uint64 major = 1;
+    uint64 minor = 2;
+    uint64 patch = 3;
+    string tag = 4;
+}
+
+message TerminateRequest{}
+
+message TerminateResponse{}
+
+service Daemon {
+    rpc Version(VersionRequest) returns (VersionResponse) {}
+    rpc Terminate(TerminateRequest) returns (TerminateResponse) {}
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.grpc.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.grpc.swift
new file mode 100644
index 00000000..aa8abe25
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.grpc.swift
@@ -0,0 +1,908 @@
+//
+// DO NOT EDIT.
+// swift-format-ignore-file
+//
+// Generated by the protocol buffer compiler.
+// Source: service_synchronization_synchronization.proto
+//
+import GRPC
+import NIO
+import NIOConcurrencyHelpers
+import SwiftProtobuf
+
+
+/// Synchronization manages the lifecycle of synchronization sessions.
+///
+/// Usage: instantiate `Synchronization_SynchronizationClient`, then call methods of this protocol to make API calls.
+internal protocol Synchronization_SynchronizationClientProtocol: GRPCClient {
+  var serviceName: String { get }
+  var interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol? { get }
+
+  func create(
+    _ request: Synchronization_CreateRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Synchronization_CreateRequest, Synchronization_CreateResponse>
+
+  func list(
+    _ request: Synchronization_ListRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Synchronization_ListRequest, Synchronization_ListResponse>
+
+  func flush(
+    _ request: Synchronization_FlushRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Synchronization_FlushRequest, Synchronization_FlushResponse>
+
+  func pause(
+    _ request: Synchronization_PauseRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Synchronization_PauseRequest, Synchronization_PauseResponse>
+
+  func resume(
+    _ request: Synchronization_ResumeRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Synchronization_ResumeRequest, Synchronization_ResumeResponse>
+
+  func reset(
+    _ request: Synchronization_ResetRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Synchronization_ResetRequest, Synchronization_ResetResponse>
+
+  func terminate(
+    _ request: Synchronization_TerminateRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Synchronization_TerminateRequest, Synchronization_TerminateResponse>
+}
+
+extension Synchronization_SynchronizationClientProtocol {
+  internal var serviceName: String {
+    return "synchronization.Synchronization"
+  }
+
+  /// Create creates a new session.
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to Create.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func create(
+    _ request: Synchronization_CreateRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Synchronization_CreateRequest, Synchronization_CreateResponse> {
+    return self.makeUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.create.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeCreateInterceptors() ?? []
+    )
+  }
+
+  /// List returns metadata for existing sessions.
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to List.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func list(
+    _ request: Synchronization_ListRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Synchronization_ListRequest, Synchronization_ListResponse> {
+    return self.makeUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.list.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeListInterceptors() ?? []
+    )
+  }
+
+  /// Flush flushes sessions.
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to Flush.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func flush(
+    _ request: Synchronization_FlushRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Synchronization_FlushRequest, Synchronization_FlushResponse> {
+    return self.makeUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.flush.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeFlushInterceptors() ?? []
+    )
+  }
+
+  /// Pause pauses sessions.
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to Pause.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func pause(
+    _ request: Synchronization_PauseRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Synchronization_PauseRequest, Synchronization_PauseResponse> {
+    return self.makeUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.pause.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makePauseInterceptors() ?? []
+    )
+  }
+
+  /// Resume resumes paused or disconnected sessions.
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to Resume.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func resume(
+    _ request: Synchronization_ResumeRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Synchronization_ResumeRequest, Synchronization_ResumeResponse> {
+    return self.makeUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.resume.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeResumeInterceptors() ?? []
+    )
+  }
+
+  /// Reset resets sessions' histories.
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to Reset.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func reset(
+    _ request: Synchronization_ResetRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Synchronization_ResetRequest, Synchronization_ResetResponse> {
+    return self.makeUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.reset.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeResetInterceptors() ?? []
+    )
+  }
+
+  /// Terminate terminates sessions.
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to Terminate.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func terminate(
+    _ request: Synchronization_TerminateRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Synchronization_TerminateRequest, Synchronization_TerminateResponse> {
+    return self.makeUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.terminate.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
+    )
+  }
+}
+
+@available(*, deprecated)
+extension Synchronization_SynchronizationClient: @unchecked Sendable {}
+
+@available(*, deprecated, renamed: "Synchronization_SynchronizationNIOClient")
+internal final class Synchronization_SynchronizationClient: Synchronization_SynchronizationClientProtocol {
+  private let lock = Lock()
+  private var _defaultCallOptions: CallOptions
+  private var _interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol?
+  internal let channel: GRPCChannel
+  internal var defaultCallOptions: CallOptions {
+    get { self.lock.withLock { return self._defaultCallOptions } }
+    set { self.lock.withLockVoid { self._defaultCallOptions = newValue } }
+  }
+  internal var interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol? {
+    get { self.lock.withLock { return self._interceptors } }
+    set { self.lock.withLockVoid { self._interceptors = newValue } }
+  }
+
+  /// Creates a client for the synchronization.Synchronization service.
+  ///
+  /// - Parameters:
+  ///   - channel: `GRPCChannel` to the service host.
+  ///   - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
+  ///   - interceptors: A factory providing interceptors for each RPC.
+  internal init(
+    channel: GRPCChannel,
+    defaultCallOptions: CallOptions = CallOptions(),
+    interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol? = nil
+  ) {
+    self.channel = channel
+    self._defaultCallOptions = defaultCallOptions
+    self._interceptors = interceptors
+  }
+}
+
+internal struct Synchronization_SynchronizationNIOClient: Synchronization_SynchronizationClientProtocol {
+  internal var channel: GRPCChannel
+  internal var defaultCallOptions: CallOptions
+  internal var interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol?
+
+  /// Creates a client for the synchronization.Synchronization service.
+  ///
+  /// - Parameters:
+  ///   - channel: `GRPCChannel` to the service host.
+  ///   - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
+  ///   - interceptors: A factory providing interceptors for each RPC.
+  internal init(
+    channel: GRPCChannel,
+    defaultCallOptions: CallOptions = CallOptions(),
+    interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol? = nil
+  ) {
+    self.channel = channel
+    self.defaultCallOptions = defaultCallOptions
+    self.interceptors = interceptors
+  }
+}
+
+/// Synchronization manages the lifecycle of synchronization sessions.
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+internal protocol Synchronization_SynchronizationAsyncClientProtocol: GRPCClient {
+  static var serviceDescriptor: GRPCServiceDescriptor { get }
+  var interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol? { get }
+
+  func makeCreateCall(
+    _ request: Synchronization_CreateRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Synchronization_CreateRequest, Synchronization_CreateResponse>
+
+  func makeListCall(
+    _ request: Synchronization_ListRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Synchronization_ListRequest, Synchronization_ListResponse>
+
+  func makeFlushCall(
+    _ request: Synchronization_FlushRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Synchronization_FlushRequest, Synchronization_FlushResponse>
+
+  func makePauseCall(
+    _ request: Synchronization_PauseRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Synchronization_PauseRequest, Synchronization_PauseResponse>
+
+  func makeResumeCall(
+    _ request: Synchronization_ResumeRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Synchronization_ResumeRequest, Synchronization_ResumeResponse>
+
+  func makeResetCall(
+    _ request: Synchronization_ResetRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Synchronization_ResetRequest, Synchronization_ResetResponse>
+
+  func makeTerminateCall(
+    _ request: Synchronization_TerminateRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Synchronization_TerminateRequest, Synchronization_TerminateResponse>
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension Synchronization_SynchronizationAsyncClientProtocol {
+  internal static var serviceDescriptor: GRPCServiceDescriptor {
+    return Synchronization_SynchronizationClientMetadata.serviceDescriptor
+  }
+
+  internal var interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol? {
+    return nil
+  }
+
+  internal func makeCreateCall(
+    _ request: Synchronization_CreateRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Synchronization_CreateRequest, Synchronization_CreateResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.create.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeCreateInterceptors() ?? []
+    )
+  }
+
+  internal func makeListCall(
+    _ request: Synchronization_ListRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Synchronization_ListRequest, Synchronization_ListResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.list.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeListInterceptors() ?? []
+    )
+  }
+
+  internal func makeFlushCall(
+    _ request: Synchronization_FlushRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Synchronization_FlushRequest, Synchronization_FlushResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.flush.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeFlushInterceptors() ?? []
+    )
+  }
+
+  internal func makePauseCall(
+    _ request: Synchronization_PauseRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Synchronization_PauseRequest, Synchronization_PauseResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.pause.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makePauseInterceptors() ?? []
+    )
+  }
+
+  internal func makeResumeCall(
+    _ request: Synchronization_ResumeRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Synchronization_ResumeRequest, Synchronization_ResumeResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.resume.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeResumeInterceptors() ?? []
+    )
+  }
+
+  internal func makeResetCall(
+    _ request: Synchronization_ResetRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Synchronization_ResetRequest, Synchronization_ResetResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.reset.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeResetInterceptors() ?? []
+    )
+  }
+
+  internal func makeTerminateCall(
+    _ request: Synchronization_TerminateRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Synchronization_TerminateRequest, Synchronization_TerminateResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.terminate.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
+    )
+  }
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension Synchronization_SynchronizationAsyncClientProtocol {
+  internal func create(
+    _ request: Synchronization_CreateRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Synchronization_CreateResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.create.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeCreateInterceptors() ?? []
+    )
+  }
+
+  internal func list(
+    _ request: Synchronization_ListRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Synchronization_ListResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.list.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeListInterceptors() ?? []
+    )
+  }
+
+  internal func flush(
+    _ request: Synchronization_FlushRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Synchronization_FlushResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.flush.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeFlushInterceptors() ?? []
+    )
+  }
+
+  internal func pause(
+    _ request: Synchronization_PauseRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Synchronization_PauseResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.pause.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makePauseInterceptors() ?? []
+    )
+  }
+
+  internal func resume(
+    _ request: Synchronization_ResumeRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Synchronization_ResumeResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.resume.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeResumeInterceptors() ?? []
+    )
+  }
+
+  internal func reset(
+    _ request: Synchronization_ResetRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Synchronization_ResetResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.reset.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeResetInterceptors() ?? []
+    )
+  }
+
+  internal func terminate(
+    _ request: Synchronization_TerminateRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Synchronization_TerminateResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Synchronization_SynchronizationClientMetadata.Methods.terminate.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
+    )
+  }
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+internal struct Synchronization_SynchronizationAsyncClient: Synchronization_SynchronizationAsyncClientProtocol {
+  internal var channel: GRPCChannel
+  internal var defaultCallOptions: CallOptions
+  internal var interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol?
+
+  internal init(
+    channel: GRPCChannel,
+    defaultCallOptions: CallOptions = CallOptions(),
+    interceptors: Synchronization_SynchronizationClientInterceptorFactoryProtocol? = nil
+  ) {
+    self.channel = channel
+    self.defaultCallOptions = defaultCallOptions
+    self.interceptors = interceptors
+  }
+}
+
+internal protocol Synchronization_SynchronizationClientInterceptorFactoryProtocol: Sendable {
+
+  /// - Returns: Interceptors to use when invoking 'create'.
+  func makeCreateInterceptors() -> [ClientInterceptor<Synchronization_CreateRequest, Synchronization_CreateResponse>]
+
+  /// - Returns: Interceptors to use when invoking 'list'.
+  func makeListInterceptors() -> [ClientInterceptor<Synchronization_ListRequest, Synchronization_ListResponse>]
+
+  /// - Returns: Interceptors to use when invoking 'flush'.
+  func makeFlushInterceptors() -> [ClientInterceptor<Synchronization_FlushRequest, Synchronization_FlushResponse>]
+
+  /// - Returns: Interceptors to use when invoking 'pause'.
+  func makePauseInterceptors() -> [ClientInterceptor<Synchronization_PauseRequest, Synchronization_PauseResponse>]
+
+  /// - Returns: Interceptors to use when invoking 'resume'.
+  func makeResumeInterceptors() -> [ClientInterceptor<Synchronization_ResumeRequest, Synchronization_ResumeResponse>]
+
+  /// - Returns: Interceptors to use when invoking 'reset'.
+  func makeResetInterceptors() -> [ClientInterceptor<Synchronization_ResetRequest, Synchronization_ResetResponse>]
+
+  /// - Returns: Interceptors to use when invoking 'terminate'.
+  func makeTerminateInterceptors() -> [ClientInterceptor<Synchronization_TerminateRequest, Synchronization_TerminateResponse>]
+}
+
+internal enum Synchronization_SynchronizationClientMetadata {
+  internal static let serviceDescriptor = GRPCServiceDescriptor(
+    name: "Synchronization",
+    fullName: "synchronization.Synchronization",
+    methods: [
+      Synchronization_SynchronizationClientMetadata.Methods.create,
+      Synchronization_SynchronizationClientMetadata.Methods.list,
+      Synchronization_SynchronizationClientMetadata.Methods.flush,
+      Synchronization_SynchronizationClientMetadata.Methods.pause,
+      Synchronization_SynchronizationClientMetadata.Methods.resume,
+      Synchronization_SynchronizationClientMetadata.Methods.reset,
+      Synchronization_SynchronizationClientMetadata.Methods.terminate,
+    ]
+  )
+
+  internal enum Methods {
+    internal static let create = GRPCMethodDescriptor(
+      name: "Create",
+      path: "/synchronization.Synchronization/Create",
+      type: GRPCCallType.unary
+    )
+
+    internal static let list = GRPCMethodDescriptor(
+      name: "List",
+      path: "/synchronization.Synchronization/List",
+      type: GRPCCallType.unary
+    )
+
+    internal static let flush = GRPCMethodDescriptor(
+      name: "Flush",
+      path: "/synchronization.Synchronization/Flush",
+      type: GRPCCallType.unary
+    )
+
+    internal static let pause = GRPCMethodDescriptor(
+      name: "Pause",
+      path: "/synchronization.Synchronization/Pause",
+      type: GRPCCallType.unary
+    )
+
+    internal static let resume = GRPCMethodDescriptor(
+      name: "Resume",
+      path: "/synchronization.Synchronization/Resume",
+      type: GRPCCallType.unary
+    )
+
+    internal static let reset = GRPCMethodDescriptor(
+      name: "Reset",
+      path: "/synchronization.Synchronization/Reset",
+      type: GRPCCallType.unary
+    )
+
+    internal static let terminate = GRPCMethodDescriptor(
+      name: "Terminate",
+      path: "/synchronization.Synchronization/Terminate",
+      type: GRPCCallType.unary
+    )
+  }
+}
+
+/// Synchronization manages the lifecycle of synchronization sessions.
+///
+/// To build a server, implement a class that conforms to this protocol.
+internal protocol Synchronization_SynchronizationProvider: CallHandlerProvider {
+  var interceptors: Synchronization_SynchronizationServerInterceptorFactoryProtocol? { get }
+
+  /// Create creates a new session.
+  func create(request: Synchronization_CreateRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Synchronization_CreateResponse>
+
+  /// List returns metadata for existing sessions.
+  func list(request: Synchronization_ListRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Synchronization_ListResponse>
+
+  /// Flush flushes sessions.
+  func flush(request: Synchronization_FlushRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Synchronization_FlushResponse>
+
+  /// Pause pauses sessions.
+  func pause(request: Synchronization_PauseRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Synchronization_PauseResponse>
+
+  /// Resume resumes paused or disconnected sessions.
+  func resume(request: Synchronization_ResumeRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Synchronization_ResumeResponse>
+
+  /// Reset resets sessions' histories.
+  func reset(request: Synchronization_ResetRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Synchronization_ResetResponse>
+
+  /// Terminate terminates sessions.
+  func terminate(request: Synchronization_TerminateRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Synchronization_TerminateResponse>
+}
+
+extension Synchronization_SynchronizationProvider {
+  internal var serviceName: Substring {
+    return Synchronization_SynchronizationServerMetadata.serviceDescriptor.fullName[...]
+  }
+
+  /// Determines, calls and returns the appropriate request handler, depending on the request's method.
+  /// Returns nil for methods not handled by this service.
+  internal func handle(
+    method name: Substring,
+    context: CallHandlerContext
+  ) -> GRPCServerHandlerProtocol? {
+    switch name {
+    case "Create":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_CreateRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_CreateResponse>(),
+        interceptors: self.interceptors?.makeCreateInterceptors() ?? [],
+        userFunction: self.create(request:context:)
+      )
+
+    case "List":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_ListRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_ListResponse>(),
+        interceptors: self.interceptors?.makeListInterceptors() ?? [],
+        userFunction: self.list(request:context:)
+      )
+
+    case "Flush":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_FlushRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_FlushResponse>(),
+        interceptors: self.interceptors?.makeFlushInterceptors() ?? [],
+        userFunction: self.flush(request:context:)
+      )
+
+    case "Pause":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_PauseRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_PauseResponse>(),
+        interceptors: self.interceptors?.makePauseInterceptors() ?? [],
+        userFunction: self.pause(request:context:)
+      )
+
+    case "Resume":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_ResumeRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_ResumeResponse>(),
+        interceptors: self.interceptors?.makeResumeInterceptors() ?? [],
+        userFunction: self.resume(request:context:)
+      )
+
+    case "Reset":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_ResetRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_ResetResponse>(),
+        interceptors: self.interceptors?.makeResetInterceptors() ?? [],
+        userFunction: self.reset(request:context:)
+      )
+
+    case "Terminate":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_TerminateRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_TerminateResponse>(),
+        interceptors: self.interceptors?.makeTerminateInterceptors() ?? [],
+        userFunction: self.terminate(request:context:)
+      )
+
+    default:
+      return nil
+    }
+  }
+}
+
+/// Synchronization manages the lifecycle of synchronization sessions.
+///
+/// To implement a server, implement an object which conforms to this protocol.
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+internal protocol Synchronization_SynchronizationAsyncProvider: CallHandlerProvider, Sendable {
+  static var serviceDescriptor: GRPCServiceDescriptor { get }
+  var interceptors: Synchronization_SynchronizationServerInterceptorFactoryProtocol? { get }
+
+  /// Create creates a new session.
+  func create(
+    request: Synchronization_CreateRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Synchronization_CreateResponse
+
+  /// List returns metadata for existing sessions.
+  func list(
+    request: Synchronization_ListRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Synchronization_ListResponse
+
+  /// Flush flushes sessions.
+  func flush(
+    request: Synchronization_FlushRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Synchronization_FlushResponse
+
+  /// Pause pauses sessions.
+  func pause(
+    request: Synchronization_PauseRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Synchronization_PauseResponse
+
+  /// Resume resumes paused or disconnected sessions.
+  func resume(
+    request: Synchronization_ResumeRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Synchronization_ResumeResponse
+
+  /// Reset resets sessions' histories.
+  func reset(
+    request: Synchronization_ResetRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Synchronization_ResetResponse
+
+  /// Terminate terminates sessions.
+  func terminate(
+    request: Synchronization_TerminateRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Synchronization_TerminateResponse
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension Synchronization_SynchronizationAsyncProvider {
+  internal static var serviceDescriptor: GRPCServiceDescriptor {
+    return Synchronization_SynchronizationServerMetadata.serviceDescriptor
+  }
+
+  internal var serviceName: Substring {
+    return Synchronization_SynchronizationServerMetadata.serviceDescriptor.fullName[...]
+  }
+
+  internal var interceptors: Synchronization_SynchronizationServerInterceptorFactoryProtocol? {
+    return nil
+  }
+
+  internal func handle(
+    method name: Substring,
+    context: CallHandlerContext
+  ) -> GRPCServerHandlerProtocol? {
+    switch name {
+    case "Create":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_CreateRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_CreateResponse>(),
+        interceptors: self.interceptors?.makeCreateInterceptors() ?? [],
+        wrapping: { try await self.create(request: $0, context: $1) }
+      )
+
+    case "List":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_ListRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_ListResponse>(),
+        interceptors: self.interceptors?.makeListInterceptors() ?? [],
+        wrapping: { try await self.list(request: $0, context: $1) }
+      )
+
+    case "Flush":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_FlushRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_FlushResponse>(),
+        interceptors: self.interceptors?.makeFlushInterceptors() ?? [],
+        wrapping: { try await self.flush(request: $0, context: $1) }
+      )
+
+    case "Pause":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_PauseRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_PauseResponse>(),
+        interceptors: self.interceptors?.makePauseInterceptors() ?? [],
+        wrapping: { try await self.pause(request: $0, context: $1) }
+      )
+
+    case "Resume":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_ResumeRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_ResumeResponse>(),
+        interceptors: self.interceptors?.makeResumeInterceptors() ?? [],
+        wrapping: { try await self.resume(request: $0, context: $1) }
+      )
+
+    case "Reset":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_ResetRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_ResetResponse>(),
+        interceptors: self.interceptors?.makeResetInterceptors() ?? [],
+        wrapping: { try await self.reset(request: $0, context: $1) }
+      )
+
+    case "Terminate":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Synchronization_TerminateRequest>(),
+        responseSerializer: ProtobufSerializer<Synchronization_TerminateResponse>(),
+        interceptors: self.interceptors?.makeTerminateInterceptors() ?? [],
+        wrapping: { try await self.terminate(request: $0, context: $1) }
+      )
+
+    default:
+      return nil
+    }
+  }
+}
+
+internal protocol Synchronization_SynchronizationServerInterceptorFactoryProtocol: Sendable {
+
+  /// - Returns: Interceptors to use when handling 'create'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makeCreateInterceptors() -> [ServerInterceptor<Synchronization_CreateRequest, Synchronization_CreateResponse>]
+
+  /// - Returns: Interceptors to use when handling 'list'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makeListInterceptors() -> [ServerInterceptor<Synchronization_ListRequest, Synchronization_ListResponse>]
+
+  /// - Returns: Interceptors to use when handling 'flush'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makeFlushInterceptors() -> [ServerInterceptor<Synchronization_FlushRequest, Synchronization_FlushResponse>]
+
+  /// - Returns: Interceptors to use when handling 'pause'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makePauseInterceptors() -> [ServerInterceptor<Synchronization_PauseRequest, Synchronization_PauseResponse>]
+
+  /// - Returns: Interceptors to use when handling 'resume'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makeResumeInterceptors() -> [ServerInterceptor<Synchronization_ResumeRequest, Synchronization_ResumeResponse>]
+
+  /// - Returns: Interceptors to use when handling 'reset'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makeResetInterceptors() -> [ServerInterceptor<Synchronization_ResetRequest, Synchronization_ResetResponse>]
+
+  /// - Returns: Interceptors to use when handling 'terminate'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makeTerminateInterceptors() -> [ServerInterceptor<Synchronization_TerminateRequest, Synchronization_TerminateResponse>]
+}
+
+internal enum Synchronization_SynchronizationServerMetadata {
+  internal static let serviceDescriptor = GRPCServiceDescriptor(
+    name: "Synchronization",
+    fullName: "synchronization.Synchronization",
+    methods: [
+      Synchronization_SynchronizationServerMetadata.Methods.create,
+      Synchronization_SynchronizationServerMetadata.Methods.list,
+      Synchronization_SynchronizationServerMetadata.Methods.flush,
+      Synchronization_SynchronizationServerMetadata.Methods.pause,
+      Synchronization_SynchronizationServerMetadata.Methods.resume,
+      Synchronization_SynchronizationServerMetadata.Methods.reset,
+      Synchronization_SynchronizationServerMetadata.Methods.terminate,
+    ]
+  )
+
+  internal enum Methods {
+    internal static let create = GRPCMethodDescriptor(
+      name: "Create",
+      path: "/synchronization.Synchronization/Create",
+      type: GRPCCallType.unary
+    )
+
+    internal static let list = GRPCMethodDescriptor(
+      name: "List",
+      path: "/synchronization.Synchronization/List",
+      type: GRPCCallType.unary
+    )
+
+    internal static let flush = GRPCMethodDescriptor(
+      name: "Flush",
+      path: "/synchronization.Synchronization/Flush",
+      type: GRPCCallType.unary
+    )
+
+    internal static let pause = GRPCMethodDescriptor(
+      name: "Pause",
+      path: "/synchronization.Synchronization/Pause",
+      type: GRPCCallType.unary
+    )
+
+    internal static let resume = GRPCMethodDescriptor(
+      name: "Resume",
+      path: "/synchronization.Synchronization/Resume",
+      type: GRPCCallType.unary
+    )
+
+    internal static let reset = GRPCMethodDescriptor(
+      name: "Reset",
+      path: "/synchronization.Synchronization/Reset",
+      type: GRPCCallType.unary
+    )
+
+    internal static let terminate = GRPCMethodDescriptor(
+      name: "Terminate",
+      path: "/synchronization.Synchronization/Terminate",
+      type: GRPCCallType.unary
+    )
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.pb.swift
new file mode 100644
index 00000000..ccb4100a
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.pb.swift
@@ -0,0 +1,1006 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: service_synchronization_synchronization.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/service/synchronization/synchronization.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// CreationSpecification contains the metadata required for a new session.
+struct Synchronization_CreationSpecification: @unchecked Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Alpha is the alpha endpoint URL for the session.
+  var alpha: Url_URL {
+    get {return _storage._alpha ?? Url_URL()}
+    set {_uniqueStorage()._alpha = newValue}
+  }
+  /// Returns true if `alpha` has been explicitly set.
+  var hasAlpha: Bool {return _storage._alpha != nil}
+  /// Clears the value of `alpha`. Subsequent reads from it will return its default value.
+  mutating func clearAlpha() {_uniqueStorage()._alpha = nil}
+
+  /// Beta is the beta endpoint URL for the session.
+  var beta: Url_URL {
+    get {return _storage._beta ?? Url_URL()}
+    set {_uniqueStorage()._beta = newValue}
+  }
+  /// Returns true if `beta` has been explicitly set.
+  var hasBeta: Bool {return _storage._beta != nil}
+  /// Clears the value of `beta`. Subsequent reads from it will return its default value.
+  mutating func clearBeta() {_uniqueStorage()._beta = nil}
+
+  /// Configuration is the base session configuration. It is the result of
+  /// merging the global configuration (unless disabled), any manually
+  /// specified configuration file, and any command line configuration
+  /// parameters.
+  var configuration: Synchronization_Configuration {
+    get {return _storage._configuration ?? Synchronization_Configuration()}
+    set {_uniqueStorage()._configuration = newValue}
+  }
+  /// Returns true if `configuration` has been explicitly set.
+  var hasConfiguration: Bool {return _storage._configuration != nil}
+  /// Clears the value of `configuration`. Subsequent reads from it will return its default value.
+  mutating func clearConfiguration() {_uniqueStorage()._configuration = nil}
+
+  /// ConfigurationAlpha is the alpha-specific session configuration. It is
+  /// determined based on command line configuration parameters.
+  var configurationAlpha: Synchronization_Configuration {
+    get {return _storage._configurationAlpha ?? Synchronization_Configuration()}
+    set {_uniqueStorage()._configurationAlpha = newValue}
+  }
+  /// Returns true if `configurationAlpha` has been explicitly set.
+  var hasConfigurationAlpha: Bool {return _storage._configurationAlpha != nil}
+  /// Clears the value of `configurationAlpha`. Subsequent reads from it will return its default value.
+  mutating func clearConfigurationAlpha() {_uniqueStorage()._configurationAlpha = nil}
+
+  /// ConfigurationBeta is the beta-specific session configuration. It is
+  /// determined based on command line configuration parameters.
+  var configurationBeta: Synchronization_Configuration {
+    get {return _storage._configurationBeta ?? Synchronization_Configuration()}
+    set {_uniqueStorage()._configurationBeta = newValue}
+  }
+  /// Returns true if `configurationBeta` has been explicitly set.
+  var hasConfigurationBeta: Bool {return _storage._configurationBeta != nil}
+  /// Clears the value of `configurationBeta`. Subsequent reads from it will return its default value.
+  mutating func clearConfigurationBeta() {_uniqueStorage()._configurationBeta = nil}
+
+  /// Name is the name for the session object.
+  var name: String {
+    get {return _storage._name}
+    set {_uniqueStorage()._name = newValue}
+  }
+
+  /// Labels are the labels for the session object.
+  var labels: Dictionary<String,String> {
+    get {return _storage._labels}
+    set {_uniqueStorage()._labels = newValue}
+  }
+
+  /// Paused indicates whether or not to create the session pre-paused.
+  var paused: Bool {
+    get {return _storage._paused}
+    set {_uniqueStorage()._paused = newValue}
+  }
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _storage = _StorageClass.defaultInstance
+}
+
+/// CreateRequest encodes a request for session creation.
+struct Synchronization_CreateRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Prompter is the prompter identifier to use for creating sessions.
+  var prompter: String = String()
+
+  /// Specification is the creation specification.
+  var specification: Synchronization_CreationSpecification {
+    get {return _specification ?? Synchronization_CreationSpecification()}
+    set {_specification = newValue}
+  }
+  /// Returns true if `specification` has been explicitly set.
+  var hasSpecification: Bool {return self._specification != nil}
+  /// Clears the value of `specification`. Subsequent reads from it will return its default value.
+  mutating func clearSpecification() {self._specification = nil}
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _specification: Synchronization_CreationSpecification? = nil
+}
+
+/// CreateResponse encodes a session creation response.
+struct Synchronization_CreateResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Session is the resulting session identifier.
+  var session: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+/// ListRequest encodes a request for session metadata.
+struct Synchronization_ListRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Selection is the session selection criteria.
+  var selection: Selection_Selection {
+    get {return _selection ?? Selection_Selection()}
+    set {_selection = newValue}
+  }
+  /// Returns true if `selection` has been explicitly set.
+  var hasSelection: Bool {return self._selection != nil}
+  /// Clears the value of `selection`. Subsequent reads from it will return its default value.
+  mutating func clearSelection() {self._selection = nil}
+
+  /// PreviousStateIndex is the previously seen state index. 0 may be provided
+  /// to force an immediate state listing.
+  var previousStateIndex: UInt64 = 0
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _selection: Selection_Selection? = nil
+}
+
+/// ListResponse encodes session metadata.
+struct Synchronization_ListResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// StateIndex is the state index associated with the session metadata.
+  var stateIndex: UInt64 = 0
+
+  /// SessionStates are the session metadata states.
+  var sessionStates: [Synchronization_State] = []
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+/// FlushRequest encodes a request to flush sessions.
+struct Synchronization_FlushRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Prompter is the prompter to use for status message updates.
+  var prompter: String = String()
+
+  /// Selection is the session selection criteria.
+  var selection: Selection_Selection {
+    get {return _selection ?? Selection_Selection()}
+    set {_selection = newValue}
+  }
+  /// Returns true if `selection` has been explicitly set.
+  var hasSelection: Bool {return self._selection != nil}
+  /// Clears the value of `selection`. Subsequent reads from it will return its default value.
+  mutating func clearSelection() {self._selection = nil}
+
+  /// SkipWait indicates whether or not the operation should avoid blocking.
+  var skipWait: Bool = false
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _selection: Selection_Selection? = nil
+}
+
+/// FlushResponse indicates completion of flush operation(s).
+struct Synchronization_FlushResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+/// PauseRequest encodes a request to pause sessions.
+struct Synchronization_PauseRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Prompter is the prompter to use for status message updates.
+  var prompter: String = String()
+
+  /// Selection is the session selection criteria.
+  var selection: Selection_Selection {
+    get {return _selection ?? Selection_Selection()}
+    set {_selection = newValue}
+  }
+  /// Returns true if `selection` has been explicitly set.
+  var hasSelection: Bool {return self._selection != nil}
+  /// Clears the value of `selection`. Subsequent reads from it will return its default value.
+  mutating func clearSelection() {self._selection = nil}
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _selection: Selection_Selection? = nil
+}
+
+/// PauseResponse indicates completion of pause operation(s).
+struct Synchronization_PauseResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+/// ResumeRequest encodes a request to resume sessions.
+struct Synchronization_ResumeRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Prompter is the prompter identifier to use for resuming sessions.
+  var prompter: String = String()
+
+  /// Selection is the session selection criteria.
+  var selection: Selection_Selection {
+    get {return _selection ?? Selection_Selection()}
+    set {_selection = newValue}
+  }
+  /// Returns true if `selection` has been explicitly set.
+  var hasSelection: Bool {return self._selection != nil}
+  /// Clears the value of `selection`. Subsequent reads from it will return its default value.
+  mutating func clearSelection() {self._selection = nil}
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _selection: Selection_Selection? = nil
+}
+
+/// ResumeResponse indicates completion of resume operation(s).
+struct Synchronization_ResumeResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+/// ResetRequest encodes a request to reset sessions.
+struct Synchronization_ResetRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Prompter is the prompter identifier to use for resetting sessions.
+  var prompter: String = String()
+
+  /// Selection is the session selection criteria.
+  var selection: Selection_Selection {
+    get {return _selection ?? Selection_Selection()}
+    set {_selection = newValue}
+  }
+  /// Returns true if `selection` has been explicitly set.
+  var hasSelection: Bool {return self._selection != nil}
+  /// Clears the value of `selection`. Subsequent reads from it will return its default value.
+  mutating func clearSelection() {self._selection = nil}
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _selection: Selection_Selection? = nil
+}
+
+/// ResetResponse indicates completion of reset operation(s).
+struct Synchronization_ResetResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+/// TerminateRequest encodes a request to terminate sessions.
+struct Synchronization_TerminateRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Prompter is the prompter to use for status message updates.
+  var prompter: String = String()
+
+  /// Selection is the session selection criteria.
+  var selection: Selection_Selection {
+    get {return _selection ?? Selection_Selection()}
+    set {_selection = newValue}
+  }
+  /// Returns true if `selection` has been explicitly set.
+  var hasSelection: Bool {return self._selection != nil}
+  /// Clears the value of `selection`. Subsequent reads from it will return its default value.
+  mutating func clearSelection() {self._selection = nil}
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _selection: Selection_Selection? = nil
+}
+
+/// TerminateResponse indicates completion of termination operation(s).
+struct Synchronization_TerminateResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "synchronization"
+
+extension Synchronization_CreationSpecification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".CreationSpecification"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "alpha"),
+    2: .same(proto: "beta"),
+    3: .same(proto: "configuration"),
+    4: .same(proto: "configurationAlpha"),
+    5: .same(proto: "configurationBeta"),
+    6: .same(proto: "name"),
+    7: .same(proto: "labels"),
+    8: .same(proto: "paused"),
+  ]
+
+  fileprivate class _StorageClass {
+    var _alpha: Url_URL? = nil
+    var _beta: Url_URL? = nil
+    var _configuration: Synchronization_Configuration? = nil
+    var _configurationAlpha: Synchronization_Configuration? = nil
+    var _configurationBeta: Synchronization_Configuration? = nil
+    var _name: String = String()
+    var _labels: Dictionary<String,String> = [:]
+    var _paused: Bool = false
+
+    #if swift(>=5.10)
+      // This property is used as the initial default value for new instances of the type.
+      // The type itself is protecting the reference to its storage via CoW semantics.
+      // This will force a copy to be made of this reference when the first mutation occurs;
+      // hence, it is safe to mark this as `nonisolated(unsafe)`.
+      static nonisolated(unsafe) let defaultInstance = _StorageClass()
+    #else
+      static let defaultInstance = _StorageClass()
+    #endif
+
+    private init() {}
+
+    init(copying source: _StorageClass) {
+      _alpha = source._alpha
+      _beta = source._beta
+      _configuration = source._configuration
+      _configurationAlpha = source._configurationAlpha
+      _configurationBeta = source._configurationBeta
+      _name = source._name
+      _labels = source._labels
+      _paused = source._paused
+    }
+  }
+
+  fileprivate mutating func _uniqueStorage() -> _StorageClass {
+    if !isKnownUniquelyReferenced(&_storage) {
+      _storage = _StorageClass(copying: _storage)
+    }
+    return _storage
+  }
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    _ = _uniqueStorage()
+    try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
+      while let fieldNumber = try decoder.nextFieldNumber() {
+        // The use of inline closures is to circumvent an issue where the compiler
+        // allocates stack space for every case branch when no optimizations are
+        // enabled. https://github.com/apple/swift-protobuf/issues/1034
+        switch fieldNumber {
+        case 1: try { try decoder.decodeSingularMessageField(value: &_storage._alpha) }()
+        case 2: try { try decoder.decodeSingularMessageField(value: &_storage._beta) }()
+        case 3: try { try decoder.decodeSingularMessageField(value: &_storage._configuration) }()
+        case 4: try { try decoder.decodeSingularMessageField(value: &_storage._configurationAlpha) }()
+        case 5: try { try decoder.decodeSingularMessageField(value: &_storage._configurationBeta) }()
+        case 6: try { try decoder.decodeSingularStringField(value: &_storage._name) }()
+        case 7: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap<SwiftProtobuf.ProtobufString,SwiftProtobuf.ProtobufString>.self, value: &_storage._labels) }()
+        case 8: try { try decoder.decodeSingularBoolField(value: &_storage._paused) }()
+        default: break
+        }
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every if/case branch local when no optimizations
+      // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+      // https://github.com/apple/swift-protobuf/issues/1182
+      try { if let v = _storage._alpha {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
+      } }()
+      try { if let v = _storage._beta {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+      } }()
+      try { if let v = _storage._configuration {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
+      } }()
+      try { if let v = _storage._configurationAlpha {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
+      } }()
+      try { if let v = _storage._configurationBeta {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
+      } }()
+      if !_storage._name.isEmpty {
+        try visitor.visitSingularStringField(value: _storage._name, fieldNumber: 6)
+      }
+      if !_storage._labels.isEmpty {
+        try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap<SwiftProtobuf.ProtobufString,SwiftProtobuf.ProtobufString>.self, value: _storage._labels, fieldNumber: 7)
+      }
+      if _storage._paused != false {
+        try visitor.visitSingularBoolField(value: _storage._paused, fieldNumber: 8)
+      }
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_CreationSpecification, rhs: Synchronization_CreationSpecification) -> Bool {
+    if lhs._storage !== rhs._storage {
+      let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
+        let _storage = _args.0
+        let rhs_storage = _args.1
+        if _storage._alpha != rhs_storage._alpha {return false}
+        if _storage._beta != rhs_storage._beta {return false}
+        if _storage._configuration != rhs_storage._configuration {return false}
+        if _storage._configurationAlpha != rhs_storage._configurationAlpha {return false}
+        if _storage._configurationBeta != rhs_storage._configurationBeta {return false}
+        if _storage._name != rhs_storage._name {return false}
+        if _storage._labels != rhs_storage._labels {return false}
+        if _storage._paused != rhs_storage._paused {return false}
+        return true
+      }
+      if !storagesAreEqual {return false}
+    }
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_CreateRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".CreateRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "prompter"),
+    2: .same(proto: "specification"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.prompter) }()
+      case 2: try { try decoder.decodeSingularMessageField(value: &self._specification) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    if !self.prompter.isEmpty {
+      try visitor.visitSingularStringField(value: self.prompter, fieldNumber: 1)
+    }
+    try { if let v = self._specification {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+    } }()
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_CreateRequest, rhs: Synchronization_CreateRequest) -> Bool {
+    if lhs.prompter != rhs.prompter {return false}
+    if lhs._specification != rhs._specification {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_CreateResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".CreateResponse"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "session"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.session) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if !self.session.isEmpty {
+      try visitor.visitSingularStringField(value: self.session, fieldNumber: 1)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_CreateResponse, rhs: Synchronization_CreateResponse) -> Bool {
+    if lhs.session != rhs.session {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_ListRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".ListRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "selection"),
+    2: .same(proto: "previousStateIndex"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularMessageField(value: &self._selection) }()
+      case 2: try { try decoder.decodeSingularUInt64Field(value: &self.previousStateIndex) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    try { if let v = self._selection {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
+    } }()
+    if self.previousStateIndex != 0 {
+      try visitor.visitSingularUInt64Field(value: self.previousStateIndex, fieldNumber: 2)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_ListRequest, rhs: Synchronization_ListRequest) -> Bool {
+    if lhs._selection != rhs._selection {return false}
+    if lhs.previousStateIndex != rhs.previousStateIndex {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_ListResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".ListResponse"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "stateIndex"),
+    2: .same(proto: "sessionStates"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularUInt64Field(value: &self.stateIndex) }()
+      case 2: try { try decoder.decodeRepeatedMessageField(value: &self.sessionStates) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if self.stateIndex != 0 {
+      try visitor.visitSingularUInt64Field(value: self.stateIndex, fieldNumber: 1)
+    }
+    if !self.sessionStates.isEmpty {
+      try visitor.visitRepeatedMessageField(value: self.sessionStates, fieldNumber: 2)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_ListResponse, rhs: Synchronization_ListResponse) -> Bool {
+    if lhs.stateIndex != rhs.stateIndex {return false}
+    if lhs.sessionStates != rhs.sessionStates {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_FlushRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".FlushRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "prompter"),
+    2: .same(proto: "selection"),
+    3: .same(proto: "skipWait"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.prompter) }()
+      case 2: try { try decoder.decodeSingularMessageField(value: &self._selection) }()
+      case 3: try { try decoder.decodeSingularBoolField(value: &self.skipWait) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    if !self.prompter.isEmpty {
+      try visitor.visitSingularStringField(value: self.prompter, fieldNumber: 1)
+    }
+    try { if let v = self._selection {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+    } }()
+    if self.skipWait != false {
+      try visitor.visitSingularBoolField(value: self.skipWait, fieldNumber: 3)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_FlushRequest, rhs: Synchronization_FlushRequest) -> Bool {
+    if lhs.prompter != rhs.prompter {return false}
+    if lhs._selection != rhs._selection {return false}
+    if lhs.skipWait != rhs.skipWait {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_FlushResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".FlushResponse"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_FlushResponse, rhs: Synchronization_FlushResponse) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_PauseRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".PauseRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "prompter"),
+    2: .same(proto: "selection"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.prompter) }()
+      case 2: try { try decoder.decodeSingularMessageField(value: &self._selection) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    if !self.prompter.isEmpty {
+      try visitor.visitSingularStringField(value: self.prompter, fieldNumber: 1)
+    }
+    try { if let v = self._selection {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+    } }()
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_PauseRequest, rhs: Synchronization_PauseRequest) -> Bool {
+    if lhs.prompter != rhs.prompter {return false}
+    if lhs._selection != rhs._selection {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_PauseResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".PauseResponse"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_PauseResponse, rhs: Synchronization_PauseResponse) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_ResumeRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".ResumeRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "prompter"),
+    2: .same(proto: "selection"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.prompter) }()
+      case 2: try { try decoder.decodeSingularMessageField(value: &self._selection) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    if !self.prompter.isEmpty {
+      try visitor.visitSingularStringField(value: self.prompter, fieldNumber: 1)
+    }
+    try { if let v = self._selection {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+    } }()
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_ResumeRequest, rhs: Synchronization_ResumeRequest) -> Bool {
+    if lhs.prompter != rhs.prompter {return false}
+    if lhs._selection != rhs._selection {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_ResumeResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".ResumeResponse"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_ResumeResponse, rhs: Synchronization_ResumeResponse) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_ResetRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".ResetRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "prompter"),
+    2: .same(proto: "selection"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.prompter) }()
+      case 2: try { try decoder.decodeSingularMessageField(value: &self._selection) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    if !self.prompter.isEmpty {
+      try visitor.visitSingularStringField(value: self.prompter, fieldNumber: 1)
+    }
+    try { if let v = self._selection {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+    } }()
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_ResetRequest, rhs: Synchronization_ResetRequest) -> Bool {
+    if lhs.prompter != rhs.prompter {return false}
+    if lhs._selection != rhs._selection {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_ResetResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".ResetResponse"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_ResetResponse, rhs: Synchronization_ResetResponse) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_TerminateRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".TerminateRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "prompter"),
+    2: .same(proto: "selection"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.prompter) }()
+      case 2: try { try decoder.decodeSingularMessageField(value: &self._selection) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    if !self.prompter.isEmpty {
+      try visitor.visitSingularStringField(value: self.prompter, fieldNumber: 1)
+    }
+    try { if let v = self._selection {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+    } }()
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_TerminateRequest, rhs: Synchronization_TerminateRequest) -> Bool {
+    if lhs.prompter != rhs.prompter {return false}
+    if lhs._selection != rhs._selection {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_TerminateResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".TerminateResponse"
+  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_TerminateResponse, rhs: Synchronization_TerminateResponse) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.proto
new file mode 100644
index 00000000..cb1ab733
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_synchronization_synchronization.proto
@@ -0,0 +1,168 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/service/synchronization/synchronization.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package synchronization;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/service/synchronization";
+
+import "selection_selection.proto";
+import "synchronization_configuration.proto";
+import "synchronization_state.proto";
+import "url_url.proto";
+
+// CreationSpecification contains the metadata required for a new session.
+message CreationSpecification {
+    // Alpha is the alpha endpoint URL for the session.
+    url.URL alpha = 1;
+    // Beta is the beta endpoint URL for the session.
+    url.URL beta = 2;
+    // Configuration is the base session configuration. It is the result of
+    // merging the global configuration (unless disabled), any manually
+    // specified configuration file, and any command line configuration
+    // parameters.
+    synchronization.Configuration configuration = 3;
+    // ConfigurationAlpha is the alpha-specific session configuration. It is
+    // determined based on command line configuration parameters.
+    synchronization.Configuration configurationAlpha = 4;
+    // ConfigurationBeta is the beta-specific session configuration. It is
+    // determined based on command line configuration parameters.
+    synchronization.Configuration configurationBeta = 5;
+    // Name is the name for the session object.
+    string name = 6;
+    // Labels are the labels for the session object.
+    map<string, string> labels = 7;
+    // Paused indicates whether or not to create the session pre-paused.
+    bool paused = 8;
+}
+
+// CreateRequest encodes a request for session creation.
+message CreateRequest {
+    // Prompter is the prompter identifier to use for creating sessions.
+    string prompter = 1;
+    // Specification is the creation specification.
+    CreationSpecification specification = 2;
+}
+
+// CreateResponse encodes a session creation response.
+message CreateResponse {
+    // Session is the resulting session identifier.
+    string session = 1;
+}
+
+// ListRequest encodes a request for session metadata.
+message ListRequest {
+    // Selection is the session selection criteria.
+    selection.Selection selection = 1;
+    // PreviousStateIndex is the previously seen state index. 0 may be provided
+    // to force an immediate state listing.
+    uint64 previousStateIndex = 2;
+}
+
+// ListResponse encodes session metadata.
+message ListResponse {
+    // StateIndex is the state index associated with the session metadata.
+    uint64 stateIndex = 1;
+    // SessionStates are the session metadata states.
+    repeated synchronization.State sessionStates = 2;
+}
+
+// FlushRequest encodes a request to flush sessions.
+message FlushRequest {
+    // Prompter is the prompter to use for status message updates.
+    string prompter = 1;
+    // Selection is the session selection criteria.
+    selection.Selection selection = 2;
+    // SkipWait indicates whether or not the operation should avoid blocking.
+    bool skipWait = 3;
+}
+
+// FlushResponse indicates completion of flush operation(s).
+message FlushResponse{}
+
+// PauseRequest encodes a request to pause sessions.
+message PauseRequest {
+    // Prompter is the prompter to use for status message updates.
+    string prompter = 1;
+    // Selection is the session selection criteria.
+    selection.Selection selection = 2;
+}
+
+// PauseResponse indicates completion of pause operation(s).
+message PauseResponse{}
+
+// ResumeRequest encodes a request to resume sessions.
+message ResumeRequest {
+    // Prompter is the prompter identifier to use for resuming sessions.
+    string prompter = 1;
+    // Selection is the session selection criteria.
+    selection.Selection selection = 2;
+}
+
+// ResumeResponse indicates completion of resume operation(s).
+message ResumeResponse{}
+
+// ResetRequest encodes a request to reset sessions.
+message ResetRequest {
+    // Prompter is the prompter identifier to use for resetting sessions.
+    string prompter = 1;
+    // Selection is the session selection criteria.
+    selection.Selection selection = 2;
+}
+
+// ResetResponse indicates completion of reset operation(s).
+message ResetResponse{}
+
+// TerminateRequest encodes a request to terminate sessions.
+message TerminateRequest {
+    // Prompter is the prompter to use for status message updates.
+    string prompter = 1;
+    // Selection is the session selection criteria.
+    selection.Selection selection = 2;
+}
+
+// TerminateResponse indicates completion of termination operation(s).
+message TerminateResponse{}
+
+// Synchronization manages the lifecycle of synchronization sessions.
+service Synchronization {
+    // Create creates a new session.
+    rpc Create(CreateRequest) returns (CreateResponse) {}
+    // List returns metadata for existing sessions.
+    rpc List(ListRequest) returns (ListResponse) {}
+    // Flush flushes sessions.
+    rpc Flush(FlushRequest) returns (FlushResponse) {}
+    // Pause pauses sessions.
+    rpc Pause(PauseRequest) returns (PauseResponse) {}
+    // Resume resumes paused or disconnected sessions.
+    rpc Resume(ResumeRequest) returns (ResumeResponse) {}
+    // Reset resets sessions' histories.
+    rpc Reset(ResetRequest) returns (ResetResponse) {}
+    // Terminate terminates sessions.
+    rpc Terminate(TerminateRequest) returns (TerminateResponse) {}
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_compression_algorithm.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_compression_algorithm.pb.swift
new file mode 100644
index 00000000..af5a42df
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_compression_algorithm.pb.swift
@@ -0,0 +1,113 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_compression_algorithm.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/compression/algorithm.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Algorithm specifies a compression algorithm.
+enum Compression_Algorithm: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// Algorithm_AlgorithmDefault represents an unspecified compression
+  /// algorithm. It should be converted to one of the following values based on
+  /// the desired default behavior.
+  case `default` // = 0
+
+  /// Algorithm_AlgorithmNone specifies that no compression should be used.
+  case none // = 1
+
+  /// Algorithm_AlgorithmDeflate specifies that DEFLATE compression should be
+  /// used.
+  case deflate // = 2
+
+  /// Algorithm_AlgorithmZstandard specifies that Zstandard compression should
+  /// be used.
+  case zstandard // = 3
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .none
+    case 2: self = .deflate
+    case 3: self = .zstandard
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .none: return 1
+    case .deflate: return 2
+    case .zstandard: return 3
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Compression_Algorithm] = [
+    .default,
+    .none,
+    .deflate,
+    .zstandard,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Compression_Algorithm: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "AlgorithmDefault"),
+    1: .same(proto: "AlgorithmNone"),
+    2: .same(proto: "AlgorithmDeflate"),
+    3: .same(proto: "AlgorithmZstandard"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_compression_algorithm.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_compression_algorithm.proto
new file mode 100644
index 00000000..ac6745e2
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_compression_algorithm.proto
@@ -0,0 +1,48 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/compression/algorithm.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package compression;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/compression";
+
+// Algorithm specifies a compression algorithm.
+enum Algorithm {
+    // Algorithm_AlgorithmDefault represents an unspecified compression
+    // algorithm. It should be converted to one of the following values based on
+    // the desired default behavior.
+    AlgorithmDefault = 0;
+    // Algorithm_AlgorithmNone specifies that no compression should be used.
+    AlgorithmNone = 1;
+    // Algorithm_AlgorithmDeflate specifies that DEFLATE compression should be
+    // used.
+    AlgorithmDeflate = 2;
+    // Algorithm_AlgorithmZstandard specifies that Zstandard compression should
+    // be used.
+    AlgorithmZstandard = 3;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_configuration.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_configuration.pb.swift
new file mode 100644
index 00000000..8ce62c70
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_configuration.pb.swift
@@ -0,0 +1,433 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_configuration.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/configuration.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Configuration encodes session configuration parameters. It is used for create
+/// commands to specify configuration options, for loading global configuration
+/// options, and for storing a merged configuration inside sessions. It should be
+/// considered immutable.
+struct Synchronization_Configuration: @unchecked Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// SynchronizationMode specifies the synchronization mode that should be
+  /// used in synchronization.
+  var synchronizationMode: Core_SynchronizationMode {
+    get {return _storage._synchronizationMode}
+    set {_uniqueStorage()._synchronizationMode = newValue}
+  }
+
+  /// HashingAlgorithm specifies the content hashing algorithm used to track
+  /// content and perform differential transfers.
+  var hashingAlgorithm: Hashing_Algorithm {
+    get {return _storage._hashingAlgorithm}
+    set {_uniqueStorage()._hashingAlgorithm = newValue}
+  }
+
+  /// MaximumEntryCount specifies the maximum number of filesystem entries that
+  /// endpoints will tolerate managing. A zero value indicates no limit.
+  var maximumEntryCount: UInt64 {
+    get {return _storage._maximumEntryCount}
+    set {_uniqueStorage()._maximumEntryCount = newValue}
+  }
+
+  /// MaximumStagingFileSize is the maximum (individual) file size that
+  /// endpoints will stage. A zero value indicates no limit.
+  var maximumStagingFileSize: UInt64 {
+    get {return _storage._maximumStagingFileSize}
+    set {_uniqueStorage()._maximumStagingFileSize = newValue}
+  }
+
+  /// ProbeMode specifies the filesystem probing mode.
+  var probeMode: Behavior_ProbeMode {
+    get {return _storage._probeMode}
+    set {_uniqueStorage()._probeMode = newValue}
+  }
+
+  /// ScanMode specifies the synchronization root scanning mode.
+  var scanMode: Synchronization_ScanMode {
+    get {return _storage._scanMode}
+    set {_uniqueStorage()._scanMode = newValue}
+  }
+
+  /// StageMode specifies the file staging mode.
+  var stageMode: Synchronization_StageMode {
+    get {return _storage._stageMode}
+    set {_uniqueStorage()._stageMode = newValue}
+  }
+
+  /// SymbolicLinkMode specifies the symbolic link mode.
+  var symbolicLinkMode: Core_SymbolicLinkMode {
+    get {return _storage._symbolicLinkMode}
+    set {_uniqueStorage()._symbolicLinkMode = newValue}
+  }
+
+  /// WatchMode specifies the filesystem watching mode.
+  var watchMode: Synchronization_WatchMode {
+    get {return _storage._watchMode}
+    set {_uniqueStorage()._watchMode = newValue}
+  }
+
+  /// WatchPollingInterval specifies the interval (in seconds) for poll-based
+  /// file monitoring. A value of 0 specifies that the default interval should
+  /// be used.
+  var watchPollingInterval: UInt32 {
+    get {return _storage._watchPollingInterval}
+    set {_uniqueStorage()._watchPollingInterval = newValue}
+  }
+
+  /// IgnoreSyntax specifies the syntax and semantics to use for ignores.
+  /// NOTE: This field is out of order due to the historical order in which it
+  /// was added.
+  var ignoreSyntax: Ignore_Syntax {
+    get {return _storage._ignoreSyntax}
+    set {_uniqueStorage()._ignoreSyntax = newValue}
+  }
+
+  /// DefaultIgnores specifies the ignore patterns brought in from the global
+  /// configuration.
+  /// DEPRECATED: This field is no longer used when loading from global
+  /// configuration. Instead, ignores provided by global configuration are
+  /// simply merged into the ignore list of the main configuration. However,
+  /// older sessions still use this field.
+  var defaultIgnores: [String] {
+    get {return _storage._defaultIgnores}
+    set {_uniqueStorage()._defaultIgnores = newValue}
+  }
+
+  /// Ignores specifies the ignore patterns brought in from the create request.
+  var ignores: [String] {
+    get {return _storage._ignores}
+    set {_uniqueStorage()._ignores = newValue}
+  }
+
+  /// IgnoreVCSMode specifies the VCS ignore mode that should be used in
+  /// synchronization.
+  var ignoreVcsmode: Ignore_IgnoreVCSMode {
+    get {return _storage._ignoreVcsmode}
+    set {_uniqueStorage()._ignoreVcsmode = newValue}
+  }
+
+  /// PermissionsMode species the manner in which permissions should be
+  /// propagated between endpoints.
+  var permissionsMode: Core_PermissionsMode {
+    get {return _storage._permissionsMode}
+    set {_uniqueStorage()._permissionsMode = newValue}
+  }
+
+  /// DefaultFileMode specifies the default permission mode to use for new
+  /// files in "portable" permission propagation mode.
+  var defaultFileMode: UInt32 {
+    get {return _storage._defaultFileMode}
+    set {_uniqueStorage()._defaultFileMode = newValue}
+  }
+
+  /// DefaultDirectoryMode specifies the default permission mode to use for new
+  /// files in "portable" permission propagation mode.
+  var defaultDirectoryMode: UInt32 {
+    get {return _storage._defaultDirectoryMode}
+    set {_uniqueStorage()._defaultDirectoryMode = newValue}
+  }
+
+  /// DefaultOwner specifies the default owner identifier to use when setting
+  /// ownership of new files and directories in "portable" permission
+  /// propagation mode.
+  var defaultOwner: String {
+    get {return _storage._defaultOwner}
+    set {_uniqueStorage()._defaultOwner = newValue}
+  }
+
+  /// DefaultGroup specifies the default group identifier to use when setting
+  /// ownership of new files and directories in "portable" permission
+  /// propagation mode.
+  var defaultGroup: String {
+    get {return _storage._defaultGroup}
+    set {_uniqueStorage()._defaultGroup = newValue}
+  }
+
+  /// CompressionAlgorithm specifies the compression algorithm to use when
+  /// communicating with the endpoint. This only applies to remote endpoints.
+  var compressionAlgorithm: Compression_Algorithm {
+    get {return _storage._compressionAlgorithm}
+    set {_uniqueStorage()._compressionAlgorithm = newValue}
+  }
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _storage = _StorageClass.defaultInstance
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "synchronization"
+
+extension Synchronization_Configuration: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".Configuration"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    11: .same(proto: "synchronizationMode"),
+    17: .same(proto: "hashingAlgorithm"),
+    12: .same(proto: "maximumEntryCount"),
+    13: .same(proto: "maximumStagingFileSize"),
+    14: .same(proto: "probeMode"),
+    15: .same(proto: "scanMode"),
+    16: .same(proto: "stageMode"),
+    1: .same(proto: "symbolicLinkMode"),
+    21: .same(proto: "watchMode"),
+    22: .same(proto: "watchPollingInterval"),
+    34: .same(proto: "ignoreSyntax"),
+    31: .same(proto: "defaultIgnores"),
+    32: .same(proto: "ignores"),
+    33: .same(proto: "ignoreVCSMode"),
+    61: .same(proto: "permissionsMode"),
+    63: .same(proto: "defaultFileMode"),
+    64: .same(proto: "defaultDirectoryMode"),
+    65: .same(proto: "defaultOwner"),
+    66: .same(proto: "defaultGroup"),
+    81: .same(proto: "compressionAlgorithm"),
+  ]
+
+  fileprivate class _StorageClass {
+    var _synchronizationMode: Core_SynchronizationMode = .default
+    var _hashingAlgorithm: Hashing_Algorithm = .default
+    var _maximumEntryCount: UInt64 = 0
+    var _maximumStagingFileSize: UInt64 = 0
+    var _probeMode: Behavior_ProbeMode = .default
+    var _scanMode: Synchronization_ScanMode = .default
+    var _stageMode: Synchronization_StageMode = .default
+    var _symbolicLinkMode: Core_SymbolicLinkMode = .default
+    var _watchMode: Synchronization_WatchMode = .default
+    var _watchPollingInterval: UInt32 = 0
+    var _ignoreSyntax: Ignore_Syntax = .default
+    var _defaultIgnores: [String] = []
+    var _ignores: [String] = []
+    var _ignoreVcsmode: Ignore_IgnoreVCSMode = .default
+    var _permissionsMode: Core_PermissionsMode = .default
+    var _defaultFileMode: UInt32 = 0
+    var _defaultDirectoryMode: UInt32 = 0
+    var _defaultOwner: String = String()
+    var _defaultGroup: String = String()
+    var _compressionAlgorithm: Compression_Algorithm = .default
+
+    #if swift(>=5.10)
+      // This property is used as the initial default value for new instances of the type.
+      // The type itself is protecting the reference to its storage via CoW semantics.
+      // This will force a copy to be made of this reference when the first mutation occurs;
+      // hence, it is safe to mark this as `nonisolated(unsafe)`.
+      static nonisolated(unsafe) let defaultInstance = _StorageClass()
+    #else
+      static let defaultInstance = _StorageClass()
+    #endif
+
+    private init() {}
+
+    init(copying source: _StorageClass) {
+      _synchronizationMode = source._synchronizationMode
+      _hashingAlgorithm = source._hashingAlgorithm
+      _maximumEntryCount = source._maximumEntryCount
+      _maximumStagingFileSize = source._maximumStagingFileSize
+      _probeMode = source._probeMode
+      _scanMode = source._scanMode
+      _stageMode = source._stageMode
+      _symbolicLinkMode = source._symbolicLinkMode
+      _watchMode = source._watchMode
+      _watchPollingInterval = source._watchPollingInterval
+      _ignoreSyntax = source._ignoreSyntax
+      _defaultIgnores = source._defaultIgnores
+      _ignores = source._ignores
+      _ignoreVcsmode = source._ignoreVcsmode
+      _permissionsMode = source._permissionsMode
+      _defaultFileMode = source._defaultFileMode
+      _defaultDirectoryMode = source._defaultDirectoryMode
+      _defaultOwner = source._defaultOwner
+      _defaultGroup = source._defaultGroup
+      _compressionAlgorithm = source._compressionAlgorithm
+    }
+  }
+
+  fileprivate mutating func _uniqueStorage() -> _StorageClass {
+    if !isKnownUniquelyReferenced(&_storage) {
+      _storage = _StorageClass(copying: _storage)
+    }
+    return _storage
+  }
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    _ = _uniqueStorage()
+    try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
+      while let fieldNumber = try decoder.nextFieldNumber() {
+        // The use of inline closures is to circumvent an issue where the compiler
+        // allocates stack space for every case branch when no optimizations are
+        // enabled. https://github.com/apple/swift-protobuf/issues/1034
+        switch fieldNumber {
+        case 1: try { try decoder.decodeSingularEnumField(value: &_storage._symbolicLinkMode) }()
+        case 11: try { try decoder.decodeSingularEnumField(value: &_storage._synchronizationMode) }()
+        case 12: try { try decoder.decodeSingularUInt64Field(value: &_storage._maximumEntryCount) }()
+        case 13: try { try decoder.decodeSingularUInt64Field(value: &_storage._maximumStagingFileSize) }()
+        case 14: try { try decoder.decodeSingularEnumField(value: &_storage._probeMode) }()
+        case 15: try { try decoder.decodeSingularEnumField(value: &_storage._scanMode) }()
+        case 16: try { try decoder.decodeSingularEnumField(value: &_storage._stageMode) }()
+        case 17: try { try decoder.decodeSingularEnumField(value: &_storage._hashingAlgorithm) }()
+        case 21: try { try decoder.decodeSingularEnumField(value: &_storage._watchMode) }()
+        case 22: try { try decoder.decodeSingularUInt32Field(value: &_storage._watchPollingInterval) }()
+        case 31: try { try decoder.decodeRepeatedStringField(value: &_storage._defaultIgnores) }()
+        case 32: try { try decoder.decodeRepeatedStringField(value: &_storage._ignores) }()
+        case 33: try { try decoder.decodeSingularEnumField(value: &_storage._ignoreVcsmode) }()
+        case 34: try { try decoder.decodeSingularEnumField(value: &_storage._ignoreSyntax) }()
+        case 61: try { try decoder.decodeSingularEnumField(value: &_storage._permissionsMode) }()
+        case 63: try { try decoder.decodeSingularUInt32Field(value: &_storage._defaultFileMode) }()
+        case 64: try { try decoder.decodeSingularUInt32Field(value: &_storage._defaultDirectoryMode) }()
+        case 65: try { try decoder.decodeSingularStringField(value: &_storage._defaultOwner) }()
+        case 66: try { try decoder.decodeSingularStringField(value: &_storage._defaultGroup) }()
+        case 81: try { try decoder.decodeSingularEnumField(value: &_storage._compressionAlgorithm) }()
+        default: break
+        }
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
+      if _storage._symbolicLinkMode != .default {
+        try visitor.visitSingularEnumField(value: _storage._symbolicLinkMode, fieldNumber: 1)
+      }
+      if _storage._synchronizationMode != .default {
+        try visitor.visitSingularEnumField(value: _storage._synchronizationMode, fieldNumber: 11)
+      }
+      if _storage._maximumEntryCount != 0 {
+        try visitor.visitSingularUInt64Field(value: _storage._maximumEntryCount, fieldNumber: 12)
+      }
+      if _storage._maximumStagingFileSize != 0 {
+        try visitor.visitSingularUInt64Field(value: _storage._maximumStagingFileSize, fieldNumber: 13)
+      }
+      if _storage._probeMode != .default {
+        try visitor.visitSingularEnumField(value: _storage._probeMode, fieldNumber: 14)
+      }
+      if _storage._scanMode != .default {
+        try visitor.visitSingularEnumField(value: _storage._scanMode, fieldNumber: 15)
+      }
+      if _storage._stageMode != .default {
+        try visitor.visitSingularEnumField(value: _storage._stageMode, fieldNumber: 16)
+      }
+      if _storage._hashingAlgorithm != .default {
+        try visitor.visitSingularEnumField(value: _storage._hashingAlgorithm, fieldNumber: 17)
+      }
+      if _storage._watchMode != .default {
+        try visitor.visitSingularEnumField(value: _storage._watchMode, fieldNumber: 21)
+      }
+      if _storage._watchPollingInterval != 0 {
+        try visitor.visitSingularUInt32Field(value: _storage._watchPollingInterval, fieldNumber: 22)
+      }
+      if !_storage._defaultIgnores.isEmpty {
+        try visitor.visitRepeatedStringField(value: _storage._defaultIgnores, fieldNumber: 31)
+      }
+      if !_storage._ignores.isEmpty {
+        try visitor.visitRepeatedStringField(value: _storage._ignores, fieldNumber: 32)
+      }
+      if _storage._ignoreVcsmode != .default {
+        try visitor.visitSingularEnumField(value: _storage._ignoreVcsmode, fieldNumber: 33)
+      }
+      if _storage._ignoreSyntax != .default {
+        try visitor.visitSingularEnumField(value: _storage._ignoreSyntax, fieldNumber: 34)
+      }
+      if _storage._permissionsMode != .default {
+        try visitor.visitSingularEnumField(value: _storage._permissionsMode, fieldNumber: 61)
+      }
+      if _storage._defaultFileMode != 0 {
+        try visitor.visitSingularUInt32Field(value: _storage._defaultFileMode, fieldNumber: 63)
+      }
+      if _storage._defaultDirectoryMode != 0 {
+        try visitor.visitSingularUInt32Field(value: _storage._defaultDirectoryMode, fieldNumber: 64)
+      }
+      if !_storage._defaultOwner.isEmpty {
+        try visitor.visitSingularStringField(value: _storage._defaultOwner, fieldNumber: 65)
+      }
+      if !_storage._defaultGroup.isEmpty {
+        try visitor.visitSingularStringField(value: _storage._defaultGroup, fieldNumber: 66)
+      }
+      if _storage._compressionAlgorithm != .default {
+        try visitor.visitSingularEnumField(value: _storage._compressionAlgorithm, fieldNumber: 81)
+      }
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_Configuration, rhs: Synchronization_Configuration) -> Bool {
+    if lhs._storage !== rhs._storage {
+      let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
+        let _storage = _args.0
+        let rhs_storage = _args.1
+        if _storage._synchronizationMode != rhs_storage._synchronizationMode {return false}
+        if _storage._hashingAlgorithm != rhs_storage._hashingAlgorithm {return false}
+        if _storage._maximumEntryCount != rhs_storage._maximumEntryCount {return false}
+        if _storage._maximumStagingFileSize != rhs_storage._maximumStagingFileSize {return false}
+        if _storage._probeMode != rhs_storage._probeMode {return false}
+        if _storage._scanMode != rhs_storage._scanMode {return false}
+        if _storage._stageMode != rhs_storage._stageMode {return false}
+        if _storage._symbolicLinkMode != rhs_storage._symbolicLinkMode {return false}
+        if _storage._watchMode != rhs_storage._watchMode {return false}
+        if _storage._watchPollingInterval != rhs_storage._watchPollingInterval {return false}
+        if _storage._ignoreSyntax != rhs_storage._ignoreSyntax {return false}
+        if _storage._defaultIgnores != rhs_storage._defaultIgnores {return false}
+        if _storage._ignores != rhs_storage._ignores {return false}
+        if _storage._ignoreVcsmode != rhs_storage._ignoreVcsmode {return false}
+        if _storage._permissionsMode != rhs_storage._permissionsMode {return false}
+        if _storage._defaultFileMode != rhs_storage._defaultFileMode {return false}
+        if _storage._defaultDirectoryMode != rhs_storage._defaultDirectoryMode {return false}
+        if _storage._defaultOwner != rhs_storage._defaultOwner {return false}
+        if _storage._defaultGroup != rhs_storage._defaultGroup {return false}
+        if _storage._compressionAlgorithm != rhs_storage._compressionAlgorithm {return false}
+        return true
+      }
+      if !storagesAreEqual {return false}
+    }
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_configuration.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_configuration.proto
new file mode 100644
index 00000000..ed613bca
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_configuration.proto
@@ -0,0 +1,174 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/configuration.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package synchronization;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization";
+
+import "filesystem_behavior_probe_mode.proto";
+import "synchronization_scan_mode.proto";
+import "synchronization_stage_mode.proto";
+import "synchronization_watch_mode.proto";
+import "synchronization_compression_algorithm.proto";
+import "synchronization_core_mode.proto";
+import "synchronization_core_permissions_mode.proto";
+import "synchronization_core_symbolic_link_mode.proto";
+import "synchronization_core_ignore_syntax.proto";
+import "synchronization_core_ignore_ignore_vcs_mode.proto";
+import "synchronization_hashing_algorithm.proto";
+
+// Configuration encodes session configuration parameters. It is used for create
+// commands to specify configuration options, for loading global configuration
+// options, and for storing a merged configuration inside sessions. It should be
+// considered immutable.
+message Configuration {
+    // Synchronization parameters (fields 11-20).
+    // NOTE: These run from field indices 11-20 (rather than 1-10, which are
+    // reserved for symbolic link configuration parameters) due to the
+    // historical order in which these fields were added. Field 17 (the digest
+    // algorithm) is also listed out of its chronological order of addition due
+    // to its relative importance in the configuration.
+
+    // SynchronizationMode specifies the synchronization mode that should be
+    // used in synchronization.
+    core.SynchronizationMode synchronizationMode = 11;
+
+    // HashingAlgorithm specifies the content hashing algorithm used to track
+    // content and perform differential transfers.
+    hashing.Algorithm hashingAlgorithm = 17;
+
+    // MaximumEntryCount specifies the maximum number of filesystem entries that
+    // endpoints will tolerate managing. A zero value indicates no limit.
+    uint64 maximumEntryCount = 12;
+
+    // MaximumStagingFileSize is the maximum (individual) file size that
+    // endpoints will stage. A zero value indicates no limit.
+    uint64 maximumStagingFileSize = 13;
+
+    // ProbeMode specifies the filesystem probing mode.
+    behavior.ProbeMode probeMode = 14;
+
+    // ScanMode specifies the synchronization root scanning mode.
+    ScanMode scanMode = 15;
+
+    // StageMode specifies the file staging mode.
+    StageMode stageMode = 16;
+
+    // Fields 18-20 are reserved for future synchronization configuration
+    // parameters.
+
+
+    // Symbolic link configuration parameters (fields 1-10).
+    // NOTE: These run from field indices 1-10. The reason for this is that
+    // symbolic link configuration parameters is due to the historical order in
+    // which configuration fields were added.
+
+    // SymbolicLinkMode specifies the symbolic link mode.
+    core.SymbolicLinkMode symbolicLinkMode = 1;
+
+    // Fields 2-10 are reserved for future symbolic link configuration
+    // parameters.
+
+
+    // Watch configuration parameters (fields 21-30).
+
+    // WatchMode specifies the filesystem watching mode.
+    WatchMode watchMode = 21;
+
+    // WatchPollingInterval specifies the interval (in seconds) for poll-based
+    // file monitoring. A value of 0 specifies that the default interval should
+    // be used.
+    uint32 watchPollingInterval = 22;
+
+    // Fields 23-30 are reserved for future watch configuration parameters.
+
+
+    // Ignore configuration parameters (fields 31-60).
+
+    // IgnoreSyntax specifies the syntax and semantics to use for ignores.
+    // NOTE: This field is out of order due to the historical order in which it
+    // was added.
+    ignore.Syntax ignoreSyntax = 34;
+
+    // DefaultIgnores specifies the ignore patterns brought in from the global
+    // configuration.
+    // DEPRECATED: This field is no longer used when loading from global
+    // configuration. Instead, ignores provided by global configuration are
+    // simply merged into the ignore list of the main configuration. However,
+    // older sessions still use this field.
+    repeated string defaultIgnores = 31;
+
+    // Ignores specifies the ignore patterns brought in from the create request.
+    repeated string ignores = 32;
+
+    // IgnoreVCSMode specifies the VCS ignore mode that should be used in
+    // synchronization.
+    ignore.IgnoreVCSMode ignoreVCSMode = 33;
+
+    // Fields 35-60 are reserved for future ignore configuration parameters.
+
+
+    // Permissions configuration parameters (fields 61-80).
+
+    // PermissionsMode species the manner in which permissions should be
+    // propagated between endpoints.
+    core.PermissionsMode permissionsMode = 61;
+
+    // Field 62 is reserved for PermissionsPreservationMode.
+
+    // DefaultFileMode specifies the default permission mode to use for new
+    // files in "portable" permission propagation mode.
+    uint32 defaultFileMode = 63;
+
+    // DefaultDirectoryMode specifies the default permission mode to use for new
+    // files in "portable" permission propagation mode.
+    uint32 defaultDirectoryMode = 64;
+
+    // DefaultOwner specifies the default owner identifier to use when setting
+    // ownership of new files and directories in "portable" permission
+    // propagation mode.
+    string defaultOwner = 65;
+
+    // DefaultGroup specifies the default group identifier to use when setting
+    // ownership of new files and directories in "portable" permission
+    // propagation mode.
+    string defaultGroup = 66;
+
+    // Fields 67-80 are reserved for future permission configuration parameters.
+
+
+    // Compression configuration parameters (fields 81-90).
+
+    // CompressionAlgorithm specifies the compression algorithm to use when
+    // communicating with the endpoint. This only applies to remote endpoints.
+    compression.Algorithm compressionAlgorithm = 81;
+
+    // Fields 82-90 are reserved for future compression configuration
+    // parameters.
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_change.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_change.pb.swift
new file mode 100644
index 00000000..5e53a588
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_change.pb.swift
@@ -0,0 +1,140 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_core_change.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/change.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Change encodes a change to an entry hierarchy. Change objects should be
+/// considered immutable and must not be modified.
+struct Core_Change: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Path is the path of the root of the change (relative to the
+  /// synchronization root).
+  var path: String = String()
+
+  /// Old represents the old filesystem hierarchy at the change path. It may be
+  /// nil if no content previously existed.
+  var old: Core_Entry {
+    get {return _old ?? Core_Entry()}
+    set {_old = newValue}
+  }
+  /// Returns true if `old` has been explicitly set.
+  var hasOld: Bool {return self._old != nil}
+  /// Clears the value of `old`. Subsequent reads from it will return its default value.
+  mutating func clearOld() {self._old = nil}
+
+  /// New represents the new filesystem hierarchy at the change path. It may be
+  /// nil if content has been deleted.
+  var new: Core_Entry {
+    get {return _new ?? Core_Entry()}
+    set {_new = newValue}
+  }
+  /// Returns true if `new` has been explicitly set.
+  var hasNew: Bool {return self._new != nil}
+  /// Clears the value of `new`. Subsequent reads from it will return its default value.
+  mutating func clearNew() {self._new = nil}
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _old: Core_Entry? = nil
+  fileprivate var _new: Core_Entry? = nil
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "core"
+
+extension Core_Change: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".Change"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "path"),
+    2: .same(proto: "old"),
+    3: .same(proto: "new"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.path) }()
+      case 2: try { try decoder.decodeSingularMessageField(value: &self._old) }()
+      case 3: try { try decoder.decodeSingularMessageField(value: &self._new) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    if !self.path.isEmpty {
+      try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
+    }
+    try { if let v = self._old {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+    } }()
+    try { if let v = self._new {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
+    } }()
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Core_Change, rhs: Core_Change) -> Bool {
+    if lhs.path != rhs.path {return false}
+    if lhs._old != rhs._old {return false}
+    if lhs._new != rhs._new {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_change.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_change.proto
new file mode 100644
index 00000000..9fc24db8
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_change.proto
@@ -0,0 +1,48 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/change.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package core;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/core";
+
+import "synchronization_core_entry.proto";
+
+// Change encodes a change to an entry hierarchy. Change objects should be
+// considered immutable and must not be modified.
+message Change {
+    // Path is the path of the root of the change (relative to the
+    // synchronization root).
+    string path = 1;
+    // Old represents the old filesystem hierarchy at the change path. It may be
+    // nil if no content previously existed.
+    Entry old = 2;
+    // New represents the new filesystem hierarchy at the change path. It may be
+    // nil if content has been deleted.
+    Entry new = 3;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_conflict.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_conflict.pb.swift
new file mode 100644
index 00000000..3607a6cb
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_conflict.pb.swift
@@ -0,0 +1,123 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_core_conflict.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/conflict.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Conflict encodes conflicting changes on alpha and beta that prevent
+/// synchronization of a particular path. Conflict objects should be considered
+/// immutable and must not be modified.
+struct Core_Conflict: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Root is the root path for the conflict (relative to the synchronization
+  /// root). While this can (in theory) be computed based on the change lists
+  /// contained within the conflict, doing so relies on those change lists
+  /// being constructed and ordered in a particular manner that's not possible
+  /// to enforce. Additionally, conflicts are often sorted by their root path,
+  /// and dynamically computing it on every sort comparison operation would be
+  /// prohibitively expensive.
+  var root: String = String()
+
+  /// AlphaChanges are the relevant changes on alpha.
+  var alphaChanges: [Core_Change] = []
+
+  /// BetaChanges are the relevant changes on beta.
+  var betaChanges: [Core_Change] = []
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "core"
+
+extension Core_Conflict: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".Conflict"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "root"),
+    2: .same(proto: "alphaChanges"),
+    3: .same(proto: "betaChanges"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.root) }()
+      case 2: try { try decoder.decodeRepeatedMessageField(value: &self.alphaChanges) }()
+      case 3: try { try decoder.decodeRepeatedMessageField(value: &self.betaChanges) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if !self.root.isEmpty {
+      try visitor.visitSingularStringField(value: self.root, fieldNumber: 1)
+    }
+    if !self.alphaChanges.isEmpty {
+      try visitor.visitRepeatedMessageField(value: self.alphaChanges, fieldNumber: 2)
+    }
+    if !self.betaChanges.isEmpty {
+      try visitor.visitRepeatedMessageField(value: self.betaChanges, fieldNumber: 3)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Core_Conflict, rhs: Core_Conflict) -> Bool {
+    if lhs.root != rhs.root {return false}
+    if lhs.alphaChanges != rhs.alphaChanges {return false}
+    if lhs.betaChanges != rhs.betaChanges {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_conflict.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_conflict.proto
new file mode 100644
index 00000000..185f6651
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_conflict.proto
@@ -0,0 +1,52 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/conflict.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package core;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/core";
+
+import "synchronization_core_change.proto";
+
+// Conflict encodes conflicting changes on alpha and beta that prevent
+// synchronization of a particular path. Conflict objects should be considered
+// immutable and must not be modified.
+message Conflict {
+    // Root is the root path for the conflict (relative to the synchronization
+    // root). While this can (in theory) be computed based on the change lists
+    // contained within the conflict, doing so relies on those change lists
+    // being constructed and ordered in a particular manner that's not possible
+    // to enforce. Additionally, conflicts are often sorted by their root path,
+    // and dynamically computing it on every sort comparison operation would be
+    // prohibitively expensive.
+    string root = 1;
+    // AlphaChanges are the relevant changes on alpha.
+    repeated Change alphaChanges = 2;
+    // BetaChanges are the relevant changes on beta.
+    repeated Change betaChanges = 3;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_entry.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_entry.pb.swift
new file mode 100644
index 00000000..d3cb6c58
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_entry.pb.swift
@@ -0,0 +1,245 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_core_entry.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/entry.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// EntryKind encodes the type of entry represented by an Entry object.
+enum Core_EntryKind: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// EntryKind_Directory indicates a directory.
+  case directory // = 0
+
+  /// EntryKind_File indicates a regular file.
+  case file // = 1
+
+  /// EntryKind_SymbolicLink indicates a symbolic link.
+  case symbolicLink // = 2
+
+  /// EntryKind_Untracked indicates content (or the root of content) that is
+  /// intentionally excluded from synchronization by Mutagen. This includes
+  /// explicitly ignored content, content that is ignored due to settings (such
+  /// as symbolic links in the "ignore" symbolic link mode), as well as content
+  /// types that Mutagen doesn't understand and/or have a way to propagate
+  /// (such as FIFOs and Unix domain sockets). This type of entry is not
+  /// synchronizable.
+  case untracked // = 100
+
+  /// EntryKind_Problematic indicates content (or the root of content) that
+  /// would normally be synchronized, but which is currently inaccessible to
+  /// scanning. This includes (but is not limited to) content that is modified
+  /// concurrently with scanning, content that is inaccessible due to
+  /// permissions, content that can't be read due to filesystem errors, content
+  /// that cannot be properly encoded given the current settings (such as
+  /// absolute symbolic links found when using the "portable" symbolic link
+  /// mode), and content that Mutagen cannot scan or watch reliably (such as
+  /// directories that are also mount points). This type of entry is not
+  /// synchronizable.
+  case problematic // = 101
+
+  /// EntryKind_PhantomDirectory indicates a directory that was recorded with
+  /// an ignore mask. This type is used to support Docker-style ignore syntax
+  /// and semantics, which allow directories to be unignored by child content
+  /// that is explicitly unignored. This type is pseudo-synchronizable; entries
+  /// containing phantom contents must have those contents reified (to tracked
+  /// or ignored directories) using ReifyPhantomDirectories before Reconcile.
+  case phantomDirectory // = 102
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .directory
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .directory
+    case 1: self = .file
+    case 2: self = .symbolicLink
+    case 100: self = .untracked
+    case 101: self = .problematic
+    case 102: self = .phantomDirectory
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .directory: return 0
+    case .file: return 1
+    case .symbolicLink: return 2
+    case .untracked: return 100
+    case .problematic: return 101
+    case .phantomDirectory: return 102
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Core_EntryKind] = [
+    .directory,
+    .file,
+    .symbolicLink,
+    .untracked,
+    .problematic,
+    .phantomDirectory,
+  ]
+
+}
+
+/// Entry encodes a filesystem entry (e.g. a directory, a file, or a symbolic
+/// link). A nil Entry represents an absence of content. An zero-value Entry
+/// represents an empty Directory. Entry objects should be considered immutable
+/// and must not be modified.
+struct Core_Entry: @unchecked Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Kind encodes the type of filesystem entry being represented.
+  var kind: Core_EntryKind = .directory
+
+  /// Contents represents a directory entry's contents. It must only be non-nil
+  /// for directory entries.
+  var contents: Dictionary<String,Core_Entry> = [:]
+
+  /// Digest represents the hash of a file entry's contents. It must only be
+  /// non-nil for file entries.
+  var digest: Data = Data()
+
+  /// Executable indicates whether or not a file entry is marked as executable.
+  /// It must only be set (if appropriate) for file entries.
+  var executable: Bool = false
+
+  /// Target is the symbolic link target for symbolic link entries. It must be
+  /// non-empty if and only if the entry is a symbolic link.
+  var target: String = String()
+
+  /// Problem indicates the relevant error for problematic content. It must be
+  /// non-empty if and only if the entry represents problematic content.
+  var problem: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "core"
+
+extension Core_EntryKind: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "Directory"),
+    1: .same(proto: "File"),
+    2: .same(proto: "SymbolicLink"),
+    100: .same(proto: "Untracked"),
+    101: .same(proto: "Problematic"),
+    102: .same(proto: "PhantomDirectory"),
+  ]
+}
+
+extension Core_Entry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".Entry"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "kind"),
+    5: .same(proto: "contents"),
+    8: .same(proto: "digest"),
+    9: .same(proto: "executable"),
+    12: .same(proto: "target"),
+    15: .same(proto: "problem"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularEnumField(value: &self.kind) }()
+      case 5: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap<SwiftProtobuf.ProtobufString,Core_Entry>.self, value: &self.contents) }()
+      case 8: try { try decoder.decodeSingularBytesField(value: &self.digest) }()
+      case 9: try { try decoder.decodeSingularBoolField(value: &self.executable) }()
+      case 12: try { try decoder.decodeSingularStringField(value: &self.target) }()
+      case 15: try { try decoder.decodeSingularStringField(value: &self.problem) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if self.kind != .directory {
+      try visitor.visitSingularEnumField(value: self.kind, fieldNumber: 1)
+    }
+    if !self.contents.isEmpty {
+      try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap<SwiftProtobuf.ProtobufString,Core_Entry>.self, value: self.contents, fieldNumber: 5)
+    }
+    if !self.digest.isEmpty {
+      try visitor.visitSingularBytesField(value: self.digest, fieldNumber: 8)
+    }
+    if self.executable != false {
+      try visitor.visitSingularBoolField(value: self.executable, fieldNumber: 9)
+    }
+    if !self.target.isEmpty {
+      try visitor.visitSingularStringField(value: self.target, fieldNumber: 12)
+    }
+    if !self.problem.isEmpty {
+      try visitor.visitSingularStringField(value: self.problem, fieldNumber: 15)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Core_Entry, rhs: Core_Entry) -> Bool {
+    if lhs.kind != rhs.kind {return false}
+    if lhs.contents != rhs.contents {return false}
+    if lhs.digest != rhs.digest {return false}
+    if lhs.executable != rhs.executable {return false}
+    if lhs.target != rhs.target {return false}
+    if lhs.problem != rhs.problem {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_entry.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_entry.proto
new file mode 100644
index 00000000..88e2cada
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_entry.proto
@@ -0,0 +1,109 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/entry.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package core;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/core";
+
+// EntryKind encodes the type of entry represented by an Entry object.
+enum EntryKind {
+    // EntryKind_Directory indicates a directory.
+    Directory = 0;
+    // EntryKind_File indicates a regular file.
+    File = 1;
+    // EntryKind_SymbolicLink indicates a symbolic link.
+    SymbolicLink = 2;
+
+    // Values 3-99 are reserved for future synchronizable entry types.
+
+    // EntryKind_Untracked indicates content (or the root of content) that is
+    // intentionally excluded from synchronization by Mutagen. This includes
+    // explicitly ignored content, content that is ignored due to settings (such
+    // as symbolic links in the "ignore" symbolic link mode), as well as content
+    // types that Mutagen doesn't understand and/or have a way to propagate
+    // (such as FIFOs and Unix domain sockets). This type of entry is not
+    // synchronizable.
+    Untracked = 100;
+    // EntryKind_Problematic indicates content (or the root of content) that
+    // would normally be synchronized, but which is currently inaccessible to
+    // scanning. This includes (but is not limited to) content that is modified
+    // concurrently with scanning, content that is inaccessible due to
+    // permissions, content that can't be read due to filesystem errors, content
+    // that cannot be properly encoded given the current settings (such as
+    // absolute symbolic links found when using the "portable" symbolic link
+    // mode), and content that Mutagen cannot scan or watch reliably (such as
+    // directories that are also mount points). This type of entry is not
+    // synchronizable.
+    Problematic = 101;
+    // EntryKind_PhantomDirectory indicates a directory that was recorded with
+    // an ignore mask. This type is used to support Docker-style ignore syntax
+    // and semantics, which allow directories to be unignored by child content
+    // that is explicitly unignored. This type is pseudo-synchronizable; entries
+    // containing phantom contents must have those contents reified (to tracked
+    // or ignored directories) using ReifyPhantomDirectories before Reconcile.
+    PhantomDirectory = 102;
+
+    // Values 102 - 199 are reserved for future unsynchronizable entry types.
+}
+
+// Entry encodes a filesystem entry (e.g. a directory, a file, or a symbolic
+// link). A nil Entry represents an absence of content. An zero-value Entry
+// represents an empty Directory. Entry objects should be considered immutable
+// and must not be modified.
+message Entry {
+    // Kind encodes the type of filesystem entry being represented.
+    EntryKind kind = 1;
+
+    // Fields 2-4 are reserved for future common entry data.
+
+    // Contents represents a directory entry's contents. It must only be non-nil
+    // for directory entries.
+    map<string, Entry> contents = 5;
+
+    // Fields 6-7 are reserved for future directory entry data.
+
+    // Digest represents the hash of a file entry's contents. It must only be
+    // non-nil for file entries.
+    bytes digest = 8;
+    // Executable indicates whether or not a file entry is marked as executable.
+    // It must only be set (if appropriate) for file entries.
+    bool executable = 9;
+
+    // Fields 10-11 are reserved for future file entry data.
+
+    // Target is the symbolic link target for symbolic link entries. It must be
+    // non-empty if and only if the entry is a symbolic link.
+    string target = 12;
+
+    // Fields 13-14 are reserved for future symbolic link entry data.
+
+    // Problem indicates the relevant error for problematic content. It must be
+    // non-empty if and only if the entry represents problematic content.
+    string problem = 15;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_ignore_vcs_mode.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_ignore_vcs_mode.pb.swift
new file mode 100644
index 00000000..396bbc5c
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_ignore_vcs_mode.pb.swift
@@ -0,0 +1,106 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_core_ignore_ignore_vcs_mode.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/ignore/ignore_vcs_mode.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// IgnoreVCSMode specifies the mode for ignoring VCS directories.
+enum Ignore_IgnoreVCSMode: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// IgnoreVCSMode_IgnoreVCSModeDefault represents an unspecified VCS ignore
+  /// mode. It is not valid for use with Scan. It should be converted to one of
+  /// the following values based on the desired default behavior.
+  case `default` // = 0
+
+  /// IgnoreVCSMode_IgnoreVCSModeIgnore indicates that VCS directories should
+  /// be ignored.
+  case ignore // = 1
+
+  /// IgnoreVCSMode_IgnoreVCSModePropagate indicates that VCS directories
+  /// should be propagated.
+  case propagate // = 2
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .ignore
+    case 2: self = .propagate
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .ignore: return 1
+    case .propagate: return 2
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Ignore_IgnoreVCSMode] = [
+    .default,
+    .ignore,
+    .propagate,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Ignore_IgnoreVCSMode: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "IgnoreVCSModeDefault"),
+    1: .same(proto: "IgnoreVCSModeIgnore"),
+    2: .same(proto: "IgnoreVCSModePropagate"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_ignore_vcs_mode.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_ignore_vcs_mode.proto
new file mode 100644
index 00000000..6714c0c9
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_ignore_vcs_mode.proto
@@ -0,0 +1,46 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/ignore/ignore_vcs_mode.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package ignore;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/core/ignore";
+
+// IgnoreVCSMode specifies the mode for ignoring VCS directories.
+enum IgnoreVCSMode {
+    // IgnoreVCSMode_IgnoreVCSModeDefault represents an unspecified VCS ignore
+    // mode. It is not valid for use with Scan. It should be converted to one of
+    // the following values based on the desired default behavior.
+    IgnoreVCSModeDefault = 0;
+    // IgnoreVCSMode_IgnoreVCSModeIgnore indicates that VCS directories should
+    // be ignored.
+    IgnoreVCSModeIgnore = 1;
+    // IgnoreVCSMode_IgnoreVCSModePropagate indicates that VCS directories
+    // should be propagated.
+    IgnoreVCSModePropagate = 2;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_syntax.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_syntax.pb.swift
new file mode 100644
index 00000000..aa516b64
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_syntax.pb.swift
@@ -0,0 +1,106 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_core_ignore_syntax.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/ignore/syntax.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Syntax specifies the syntax and semantics for ignore specifications.
+enum Ignore_Syntax: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// Syntax_SyntaxDefault represents an unspecified ignore syntax. It is not
+  /// valid for use with core synchronization functions. It should be converted
+  /// to one of the following values based on the desired default behavior.
+  case `default` // = 0
+
+  /// Syntax_SyntaxMutagen specifies that Mutagen-style ignore syntax and
+  /// semantics should be used.
+  case mutagen // = 1
+
+  /// Syntax_SyntaxDocker specifies that Docker-style ignore syntax and
+  /// semantics should be used.
+  case docker // = 2
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .mutagen
+    case 2: self = .docker
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .mutagen: return 1
+    case .docker: return 2
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Ignore_Syntax] = [
+    .default,
+    .mutagen,
+    .docker,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Ignore_Syntax: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "SyntaxDefault"),
+    1: .same(proto: "SyntaxMutagen"),
+    2: .same(proto: "SyntaxDocker"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_syntax.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_syntax.proto
new file mode 100644
index 00000000..93468976
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_ignore_syntax.proto
@@ -0,0 +1,46 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/ignore/syntax.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package ignore;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/core/ignore";
+
+// Syntax specifies the syntax and semantics for ignore specifications.
+enum Syntax {
+    // Syntax_SyntaxDefault represents an unspecified ignore syntax. It is not
+    // valid for use with core synchronization functions. It should be converted
+    // to one of the following values based on the desired default behavior.
+    SyntaxDefault = 0;
+    // Syntax_SyntaxMutagen specifies that Mutagen-style ignore syntax and
+    // semantics should be used.
+    SyntaxMutagen = 1;
+    // Syntax_SyntaxDocker specifies that Docker-style ignore syntax and
+    // semantics should be used.
+    SyntaxDocker = 2;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_mode.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_mode.pb.swift
new file mode 100644
index 00000000..4bca523e
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_mode.pb.swift
@@ -0,0 +1,135 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_core_mode.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/mode.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// SynchronizationMode specifies the mode for synchronization, encoding both
+/// directionality and conflict resolution behavior.
+enum Core_SynchronizationMode: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// SynchronizationMode_SynchronizationModeDefault represents an unspecified
+  /// synchronization mode. It is not valid for use with Reconcile. It should
+  /// be converted to one of the following values based on the desired default
+  /// behavior.
+  case `default` // = 0
+
+  /// SynchronizationMode_SynchronizationModeTwoWaySafe represents a
+  /// bidirectional synchronization mode where automatic conflict resolution is
+  /// performed only in cases where no data would be lost. Specifically, this
+  /// means that modified contents are allowed to propagate to the opposite
+  /// endpoint if the corresponding contents on the opposite endpoint are
+  /// unmodified or deleted. All other conflicts are left unresolved.
+  case twoWaySafe // = 1
+
+  /// SynchronizationMode_SynchronizationModeTwoWayResolved is the same as
+  /// SynchronizationMode_SynchronizationModeTwoWaySafe, but specifies that the
+  /// alpha endpoint should win automatically in any conflict between alpha and
+  /// beta, including cases where alpha has deleted contents that beta has
+  /// modified.
+  case twoWayResolved // = 2
+
+  /// SynchronizationMode_SynchronizationModeOneWaySafe represents a
+  /// unidirectional synchronization mode where contents and changes propagate
+  /// from alpha to beta, but won't overwrite any creations or modifications on
+  /// beta.
+  case oneWaySafe // = 3
+
+  /// SynchronizationMode_SynchronizationModeOneWayReplica represents a
+  /// unidirectional synchronization mode where contents on alpha are mirrored
+  /// (verbatim) to beta, overwriting any conflicting contents on beta and
+  /// deleting any extraneous contents on beta.
+  case oneWayReplica // = 4
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .twoWaySafe
+    case 2: self = .twoWayResolved
+    case 3: self = .oneWaySafe
+    case 4: self = .oneWayReplica
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .twoWaySafe: return 1
+    case .twoWayResolved: return 2
+    case .oneWaySafe: return 3
+    case .oneWayReplica: return 4
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Core_SynchronizationMode] = [
+    .default,
+    .twoWaySafe,
+    .twoWayResolved,
+    .oneWaySafe,
+    .oneWayReplica,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Core_SynchronizationMode: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "SynchronizationModeDefault"),
+    1: .same(proto: "SynchronizationModeTwoWaySafe"),
+    2: .same(proto: "SynchronizationModeTwoWayResolved"),
+    3: .same(proto: "SynchronizationModeOneWaySafe"),
+    4: .same(proto: "SynchronizationModeOneWayReplica"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_mode.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_mode.proto
new file mode 100644
index 00000000..212daf70
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_mode.proto
@@ -0,0 +1,69 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/mode.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package core;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/core";
+
+// SynchronizationMode specifies the mode for synchronization, encoding both
+// directionality and conflict resolution behavior.
+enum SynchronizationMode {
+    // SynchronizationMode_SynchronizationModeDefault represents an unspecified
+    // synchronization mode. It is not valid for use with Reconcile. It should
+    // be converted to one of the following values based on the desired default
+    // behavior.
+    SynchronizationModeDefault = 0;
+
+    // SynchronizationMode_SynchronizationModeTwoWaySafe represents a
+    // bidirectional synchronization mode where automatic conflict resolution is
+    // performed only in cases where no data would be lost. Specifically, this
+    // means that modified contents are allowed to propagate to the opposite
+    // endpoint if the corresponding contents on the opposite endpoint are
+    // unmodified or deleted. All other conflicts are left unresolved.
+    SynchronizationModeTwoWaySafe = 1;
+
+    // SynchronizationMode_SynchronizationModeTwoWayResolved is the same as
+    // SynchronizationMode_SynchronizationModeTwoWaySafe, but specifies that the
+    // alpha endpoint should win automatically in any conflict between alpha and
+    // beta, including cases where alpha has deleted contents that beta has
+    // modified.
+    SynchronizationModeTwoWayResolved = 2;
+
+    // SynchronizationMode_SynchronizationModeOneWaySafe represents a
+    // unidirectional synchronization mode where contents and changes propagate
+    // from alpha to beta, but won't overwrite any creations or modifications on
+    // beta.
+    SynchronizationModeOneWaySafe = 3;
+
+    // SynchronizationMode_SynchronizationModeOneWayReplica represents a
+    // unidirectional synchronization mode where contents on alpha are mirrored
+    // (verbatim) to beta, overwriting any conflicting contents on beta and
+    // deleting any extraneous contents on beta.
+    SynchronizationModeOneWayReplica = 4;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_permissions_mode.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_permissions_mode.pb.swift
new file mode 100644
index 00000000..e6d95973
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_permissions_mode.pb.swift
@@ -0,0 +1,110 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_core_permissions_mode.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/permissions_mode.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// PermissionsMode specifies the mode for handling permission propagation.
+enum Core_PermissionsMode: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// PermissionsMode_PermissionsModeDefault represents an unspecified
+  /// permissions mode. It is not valid for use with Scan. It should be
+  /// converted to one of the following values based on the desired default
+  /// behavior.
+  case `default` // = 0
+
+  /// PermissionsMode_PermissionsModePortable specifies that permissions should
+  /// be propagated in a portable fashion. This means that only executability
+  /// bits are managed by Mutagen and that manual specifications for ownership
+  /// and base file permissions are used.
+  case portable // = 1
+
+  /// PermissionsMode_PermissionsModeManual specifies that only manual
+  /// permission specifications should be used. In this case, Mutagen does not
+  /// perform any propagation of permissions.
+  case manual // = 2
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .portable
+    case 2: self = .manual
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .portable: return 1
+    case .manual: return 2
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Core_PermissionsMode] = [
+    .default,
+    .portable,
+    .manual,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Core_PermissionsMode: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "PermissionsModeDefault"),
+    1: .same(proto: "PermissionsModePortable"),
+    2: .same(proto: "PermissionsModeManual"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_permissions_mode.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_permissions_mode.proto
new file mode 100644
index 00000000..98caa326
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_permissions_mode.proto
@@ -0,0 +1,50 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/permissions_mode.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package core;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/core";
+
+// PermissionsMode specifies the mode for handling permission propagation.
+enum PermissionsMode {
+    // PermissionsMode_PermissionsModeDefault represents an unspecified
+    // permissions mode. It is not valid for use with Scan. It should be
+    // converted to one of the following values based on the desired default
+    // behavior.
+    PermissionsModeDefault = 0;
+    // PermissionsMode_PermissionsModePortable specifies that permissions should
+    // be propagated in a portable fashion. This means that only executability
+    // bits are managed by Mutagen and that manual specifications for ownership
+    // and base file permissions are used.
+    PermissionsModePortable = 1;
+    // PermissionsMode_PermissionsModeManual specifies that only manual
+    // permission specifications should be used. In this case, Mutagen does not
+    // perform any propagation of permissions.
+    PermissionsModeManual = 2;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_problem.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_problem.pb.swift
new file mode 100644
index 00000000..8c2ba6bb
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_problem.pb.swift
@@ -0,0 +1,109 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_core_problem.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/problem.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Problem indicates an issue or error encountered at some stage of a
+/// synchronization cycle. Problem objects should be considered immutable and
+/// must not be modified.
+struct Core_Problem: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Path is the path at which the problem occurred (relative to the
+  /// synchronization root).
+  var path: String = String()
+
+  /// Error is a human-readable summary of the problem.
+  var error: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "core"
+
+extension Core_Problem: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".Problem"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "path"),
+    2: .same(proto: "error"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.path) }()
+      case 2: try { try decoder.decodeSingularStringField(value: &self.error) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if !self.path.isEmpty {
+      try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
+    }
+    if !self.error.isEmpty {
+      try visitor.visitSingularStringField(value: self.error, fieldNumber: 2)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Core_Problem, rhs: Core_Problem) -> Bool {
+    if lhs.path != rhs.path {return false}
+    if lhs.error != rhs.error {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_problem.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_problem.proto
new file mode 100644
index 00000000..2ff66107
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_problem.proto
@@ -0,0 +1,43 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/problem.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package core;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/core";
+
+// Problem indicates an issue or error encountered at some stage of a
+// synchronization cycle. Problem objects should be considered immutable and
+// must not be modified.
+message Problem {
+    // Path is the path at which the problem occurred (relative to the
+    // synchronization root).
+    string path = 1;
+    // Error is a human-readable summary of the problem.
+    string error = 2;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_symbolic_link_mode.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_symbolic_link_mode.pb.swift
new file mode 100644
index 00000000..d379c68e
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_symbolic_link_mode.pb.swift
@@ -0,0 +1,118 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_core_symbolic_link_mode.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/symbolic_link_mode.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// SymbolicLinkMode specifies the mode for handling symbolic links.
+enum Core_SymbolicLinkMode: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// SymbolicLinkMode_SymbolicLinkModeDefault represents an unspecified
+  /// symbolic link mode. It is not valid for use with Scan or Transition. It
+  /// should be converted to one of the following values based on the desired
+  /// default behavior.
+  case `default` // = 0
+
+  /// SymbolicLinkMode_SymbolicLinkModeIgnore specifies that all symbolic links
+  /// should be ignored.
+  case ignore // = 1
+
+  /// SymbolicLinkMode_SymbolicLinkModePortable specifies that only portable
+  /// symbolic links should be synchronized. Any absolute symbolic links or
+  /// symbolic links which are otherwise non-portable will be treate as
+  /// problematic content.
+  case portable // = 2
+
+  /// SymbolicLinkMode_SymbolicLinkModePOSIXRaw specifies that symbolic links
+  /// should be propagated in their raw form. It is only valid on POSIX systems
+  /// and only makes sense in the context of POSIX-to-POSIX synchronization.
+  case posixraw // = 3
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .ignore
+    case 2: self = .portable
+    case 3: self = .posixraw
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .ignore: return 1
+    case .portable: return 2
+    case .posixraw: return 3
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Core_SymbolicLinkMode] = [
+    .default,
+    .ignore,
+    .portable,
+    .posixraw,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Core_SymbolicLinkMode: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "SymbolicLinkModeDefault"),
+    1: .same(proto: "SymbolicLinkModeIgnore"),
+    2: .same(proto: "SymbolicLinkModePortable"),
+    3: .same(proto: "SymbolicLinkModePOSIXRaw"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_symbolic_link_mode.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_symbolic_link_mode.proto
new file mode 100644
index 00000000..02292961
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_core_symbolic_link_mode.proto
@@ -0,0 +1,53 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/core/symbolic_link_mode.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package core;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/core";
+
+// SymbolicLinkMode specifies the mode for handling symbolic links.
+enum SymbolicLinkMode {
+    // SymbolicLinkMode_SymbolicLinkModeDefault represents an unspecified
+    // symbolic link mode. It is not valid for use with Scan or Transition. It
+    // should be converted to one of the following values based on the desired
+    // default behavior.
+    SymbolicLinkModeDefault = 0;
+    // SymbolicLinkMode_SymbolicLinkModeIgnore specifies that all symbolic links
+    // should be ignored.
+    SymbolicLinkModeIgnore = 1;
+    // SymbolicLinkMode_SymbolicLinkModePortable specifies that only portable
+    // symbolic links should be synchronized. Any absolute symbolic links or
+    // symbolic links which are otherwise non-portable will be treate as
+    // problematic content.
+    SymbolicLinkModePortable = 2;
+    // SymbolicLinkMode_SymbolicLinkModePOSIXRaw specifies that symbolic links
+    // should be propagated in their raw form. It is only valid on POSIX systems
+    // and only makes sense in the context of POSIX-to-POSIX synchronization.
+    SymbolicLinkModePOSIXRaw = 3;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_hashing_algorithm.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_hashing_algorithm.pb.swift
new file mode 100644
index 00000000..5a9c295f
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_hashing_algorithm.pb.swift
@@ -0,0 +1,111 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_hashing_algorithm.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/hashing/algorithm.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Algorithm specifies a hashing algorithm.
+enum Hashing_Algorithm: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// Algorithm_AlgorithmDefault represents an unspecified hashing algorithm.
+  /// It should be converted to one of the following values based on the
+  /// desired default behavior.
+  case `default` // = 0
+
+  /// Algorithm_AlgorithmSHA1 specifies that SHA-1 hashing should be used.
+  case sha1 // = 1
+
+  /// Algorithm_AlgorithmSHA256 specifies that SHA-256 hashing should be used.
+  case sha256 // = 2
+
+  /// Algorithm_AlgorithmXXH128 specifies that XXH128 hashing should be used.
+  case xxh128 // = 3
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .sha1
+    case 2: self = .sha256
+    case 3: self = .xxh128
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .sha1: return 1
+    case .sha256: return 2
+    case .xxh128: return 3
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Hashing_Algorithm] = [
+    .default,
+    .sha1,
+    .sha256,
+    .xxh128,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Hashing_Algorithm: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "AlgorithmDefault"),
+    1: .same(proto: "AlgorithmSHA1"),
+    2: .same(proto: "AlgorithmSHA256"),
+    3: .same(proto: "AlgorithmXXH128"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_hashing_algorithm.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_hashing_algorithm.proto
new file mode 100644
index 00000000..a4837bc2
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_hashing_algorithm.proto
@@ -0,0 +1,46 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/hashing/algorithm.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package hashing;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/hashing";
+
+// Algorithm specifies a hashing algorithm.
+enum Algorithm {
+    // Algorithm_AlgorithmDefault represents an unspecified hashing algorithm.
+    // It should be converted to one of the following values based on the
+    // desired default behavior.
+    AlgorithmDefault = 0;
+    // Algorithm_AlgorithmSHA1 specifies that SHA-1 hashing should be used.
+    AlgorithmSHA1 = 1;
+    // Algorithm_AlgorithmSHA256 specifies that SHA-256 hashing should be used.
+    AlgorithmSHA256 = 2;
+    // Algorithm_AlgorithmXXH128 specifies that XXH128 hashing should be used.
+    AlgorithmXXH128 = 3;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_rsync_receive.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_rsync_receive.pb.swift
new file mode 100644
index 00000000..324659c6
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_rsync_receive.pb.swift
@@ -0,0 +1,145 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_rsync_receive.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/rsync/receive.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// ReceiverState encodes that status of an rsync receiver. It should be
+/// considered immutable.
+struct Rsync_ReceiverState: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Path is the path currently being received.
+  var path: String = String()
+
+  /// ReceivedSize is the number of bytes that have been received for the
+  /// current path from both block and data operations.
+  var receivedSize: UInt64 = 0
+
+  /// ExpectedSize is the number of bytes expected for the current path.
+  var expectedSize: UInt64 = 0
+
+  /// ReceivedFiles is the number of files that have already been received.
+  var receivedFiles: UInt64 = 0
+
+  /// ExpectedFiles is the total number of files expected.
+  var expectedFiles: UInt64 = 0
+
+  /// TotalReceivedSize is the total number of bytes that have been received
+  /// for all files from both block and data operations.
+  var totalReceivedSize: UInt64 = 0
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "rsync"
+
+extension Rsync_ReceiverState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".ReceiverState"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "path"),
+    2: .same(proto: "receivedSize"),
+    3: .same(proto: "expectedSize"),
+    4: .same(proto: "receivedFiles"),
+    5: .same(proto: "expectedFiles"),
+    6: .same(proto: "totalReceivedSize"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.path) }()
+      case 2: try { try decoder.decodeSingularUInt64Field(value: &self.receivedSize) }()
+      case 3: try { try decoder.decodeSingularUInt64Field(value: &self.expectedSize) }()
+      case 4: try { try decoder.decodeSingularUInt64Field(value: &self.receivedFiles) }()
+      case 5: try { try decoder.decodeSingularUInt64Field(value: &self.expectedFiles) }()
+      case 6: try { try decoder.decodeSingularUInt64Field(value: &self.totalReceivedSize) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if !self.path.isEmpty {
+      try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
+    }
+    if self.receivedSize != 0 {
+      try visitor.visitSingularUInt64Field(value: self.receivedSize, fieldNumber: 2)
+    }
+    if self.expectedSize != 0 {
+      try visitor.visitSingularUInt64Field(value: self.expectedSize, fieldNumber: 3)
+    }
+    if self.receivedFiles != 0 {
+      try visitor.visitSingularUInt64Field(value: self.receivedFiles, fieldNumber: 4)
+    }
+    if self.expectedFiles != 0 {
+      try visitor.visitSingularUInt64Field(value: self.expectedFiles, fieldNumber: 5)
+    }
+    if self.totalReceivedSize != 0 {
+      try visitor.visitSingularUInt64Field(value: self.totalReceivedSize, fieldNumber: 6)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Rsync_ReceiverState, rhs: Rsync_ReceiverState) -> Bool {
+    if lhs.path != rhs.path {return false}
+    if lhs.receivedSize != rhs.receivedSize {return false}
+    if lhs.expectedSize != rhs.expectedSize {return false}
+    if lhs.receivedFiles != rhs.receivedFiles {return false}
+    if lhs.expectedFiles != rhs.expectedFiles {return false}
+    if lhs.totalReceivedSize != rhs.totalReceivedSize {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_rsync_receive.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_rsync_receive.proto
new file mode 100644
index 00000000..43bad22e
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_rsync_receive.proto
@@ -0,0 +1,56 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/rsync/receive.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package rsync;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization/rsync";
+
+// ReceiverState encodes that status of an rsync receiver. It should be
+// considered immutable.
+message ReceiverState {
+    // Path is the path currently being received.
+    string path = 1;
+    // ReceivedSize is the number of bytes that have been received for the
+    // current path from both block and data operations.
+    uint64 receivedSize = 2;
+    // ExpectedSize is the number of bytes expected for the current path.
+    uint64 expectedSize = 3;
+    // ReceivedFiles is the number of files that have already been received.
+    uint64 receivedFiles = 4;
+    // ExpectedFiles is the total number of files expected.
+    uint64 expectedFiles = 5;
+    // TotalReceivedSize is the total number of bytes that have been received
+    // for all files from both block and data operations.
+    uint64 totalReceivedSize = 6;
+    // TODO: We may want to add statistics on the speedup offered by the rsync
+    // algorithm in terms of data volume, though obviously this can't account
+    // for any savings that might come from compression at the transport layer.
+    // It would also be really nice to have TotalExpectedSize, but this is
+    // prohibitively difficult and expensive to compute.
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_scan_mode.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_scan_mode.pb.swift
new file mode 100644
index 00000000..4d0ad6f7
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_scan_mode.pb.swift
@@ -0,0 +1,106 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_scan_mode.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/scan_mode.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// ScanMode specifies the mode for synchronization root scanning.
+enum Synchronization_ScanMode: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// ScanMode_ScanModeDefault represents an unspecified scan mode. It should
+  /// be converted to one of the following values based on the desired default
+  /// behavior.
+  case `default` // = 0
+
+  /// ScanMode_ScanModeFull specifies that full scans should be performed on
+  /// each synchronization cycle.
+  case full // = 1
+
+  /// ScanMode_ScanModeAccelerated specifies that scans should attempt to use
+  /// watch-based acceleration.
+  case accelerated // = 2
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .full
+    case 2: self = .accelerated
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .full: return 1
+    case .accelerated: return 2
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Synchronization_ScanMode] = [
+    .default,
+    .full,
+    .accelerated,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Synchronization_ScanMode: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "ScanModeDefault"),
+    1: .same(proto: "ScanModeFull"),
+    2: .same(proto: "ScanModeAccelerated"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_scan_mode.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_scan_mode.proto
new file mode 100644
index 00000000..c95f0e33
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_scan_mode.proto
@@ -0,0 +1,46 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/scan_mode.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package synchronization;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization";
+
+// ScanMode specifies the mode for synchronization root scanning.
+enum ScanMode {
+    // ScanMode_ScanModeDefault represents an unspecified scan mode. It should
+    // be converted to one of the following values based on the desired default
+    // behavior.
+    ScanModeDefault = 0;
+    // ScanMode_ScanModeFull specifies that full scans should be performed on
+    // each synchronization cycle.
+    ScanModeFull = 1;
+    // ScanMode_ScanModeAccelerated specifies that scans should attempt to use
+    // watch-based acceleration.
+    ScanModeAccelerated = 2;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_session.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_session.pb.swift
new file mode 100644
index 00000000..652166f2
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_session.pb.swift
@@ -0,0 +1,370 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_session.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/session.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Session represents a synchronization session configuration and persistent
+/// state. It is mutable within the context of the daemon, so it should be
+/// accessed and modified in a synchronized fashion. Outside of the daemon (e.g.
+/// when returned via the API), it should be considered immutable.
+struct Synchronization_Session: @unchecked Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Identifier is the (unique) session identifier. It is static. It cannot be
+  /// empty.
+  var identifier: String {
+    get {return _storage._identifier}
+    set {_uniqueStorage()._identifier = newValue}
+  }
+
+  /// Version is the session version. It is static.
+  var version: Synchronization_Version {
+    get {return _storage._version}
+    set {_uniqueStorage()._version = newValue}
+  }
+
+  /// CreationTime is the creation time of the session. It is static. It cannot
+  /// be nil.
+  var creationTime: SwiftProtobuf.Google_Protobuf_Timestamp {
+    get {return _storage._creationTime ?? SwiftProtobuf.Google_Protobuf_Timestamp()}
+    set {_uniqueStorage()._creationTime = newValue}
+  }
+  /// Returns true if `creationTime` has been explicitly set.
+  var hasCreationTime: Bool {return _storage._creationTime != nil}
+  /// Clears the value of `creationTime`. Subsequent reads from it will return its default value.
+  mutating func clearCreationTime() {_uniqueStorage()._creationTime = nil}
+
+  /// CreatingVersionMajor is the major version component of the version of
+  /// Mutagen which created the session. It is static.
+  var creatingVersionMajor: UInt32 {
+    get {return _storage._creatingVersionMajor}
+    set {_uniqueStorage()._creatingVersionMajor = newValue}
+  }
+
+  /// CreatingVersionMinor is the minor version component of the version of
+  /// Mutagen which created the session. It is static.
+  var creatingVersionMinor: UInt32 {
+    get {return _storage._creatingVersionMinor}
+    set {_uniqueStorage()._creatingVersionMinor = newValue}
+  }
+
+  /// CreatingVersionPatch is the patch version component of the version of
+  /// Mutagen which created the session. It is static.
+  var creatingVersionPatch: UInt32 {
+    get {return _storage._creatingVersionPatch}
+    set {_uniqueStorage()._creatingVersionPatch = newValue}
+  }
+
+  /// Alpha is the alpha endpoint URL. It is static. It cannot be nil.
+  var alpha: Url_URL {
+    get {return _storage._alpha ?? Url_URL()}
+    set {_uniqueStorage()._alpha = newValue}
+  }
+  /// Returns true if `alpha` has been explicitly set.
+  var hasAlpha: Bool {return _storage._alpha != nil}
+  /// Clears the value of `alpha`. Subsequent reads from it will return its default value.
+  mutating func clearAlpha() {_uniqueStorage()._alpha = nil}
+
+  /// Beta is the beta endpoint URL. It is static. It cannot be nil.
+  var beta: Url_URL {
+    get {return _storage._beta ?? Url_URL()}
+    set {_uniqueStorage()._beta = newValue}
+  }
+  /// Returns true if `beta` has been explicitly set.
+  var hasBeta: Bool {return _storage._beta != nil}
+  /// Clears the value of `beta`. Subsequent reads from it will return its default value.
+  mutating func clearBeta() {_uniqueStorage()._beta = nil}
+
+  /// Configuration is the flattened session configuration. It is static. It
+  /// cannot be nil.
+  var configuration: Synchronization_Configuration {
+    get {return _storage._configuration ?? Synchronization_Configuration()}
+    set {_uniqueStorage()._configuration = newValue}
+  }
+  /// Returns true if `configuration` has been explicitly set.
+  var hasConfiguration: Bool {return _storage._configuration != nil}
+  /// Clears the value of `configuration`. Subsequent reads from it will return its default value.
+  mutating func clearConfiguration() {_uniqueStorage()._configuration = nil}
+
+  /// ConfigurationAlpha are the alpha-specific session configuration
+  /// overrides. It is static. It may be nil for existing sessions loaded from
+  /// disk, but it is not considered valid unless non-nil, so it should be
+  /// replaced with an empty default value in-memory if a nil on-disk value is
+  /// detected.
+  var configurationAlpha: Synchronization_Configuration {
+    get {return _storage._configurationAlpha ?? Synchronization_Configuration()}
+    set {_uniqueStorage()._configurationAlpha = newValue}
+  }
+  /// Returns true if `configurationAlpha` has been explicitly set.
+  var hasConfigurationAlpha: Bool {return _storage._configurationAlpha != nil}
+  /// Clears the value of `configurationAlpha`. Subsequent reads from it will return its default value.
+  mutating func clearConfigurationAlpha() {_uniqueStorage()._configurationAlpha = nil}
+
+  /// ConfigurationBeta are the beta-specific session configuration overrides.
+  /// It is static. It may be nil for existing sessions loaded from disk, but
+  /// it is not considered valid unless non-nil, so it should be replaced with
+  /// an empty default value in-memory if a nil on-disk value is detected.
+  var configurationBeta: Synchronization_Configuration {
+    get {return _storage._configurationBeta ?? Synchronization_Configuration()}
+    set {_uniqueStorage()._configurationBeta = newValue}
+  }
+  /// Returns true if `configurationBeta` has been explicitly set.
+  var hasConfigurationBeta: Bool {return _storage._configurationBeta != nil}
+  /// Clears the value of `configurationBeta`. Subsequent reads from it will return its default value.
+  mutating func clearConfigurationBeta() {_uniqueStorage()._configurationBeta = nil}
+
+  /// Name is a user-friendly name for the session. It may be empty and is not
+  /// guaranteed to be unique across all sessions. It is only used as a simpler
+  /// handle for specifying sessions. It is static.
+  var name: String {
+    get {return _storage._name}
+    set {_uniqueStorage()._name = newValue}
+  }
+
+  /// Labels are the session labels. They are static.
+  var labels: Dictionary<String,String> {
+    get {return _storage._labels}
+    set {_uniqueStorage()._labels = newValue}
+  }
+
+  /// Paused indicates whether or not the session is marked as paused.
+  var paused: Bool {
+    get {return _storage._paused}
+    set {_uniqueStorage()._paused = newValue}
+  }
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _storage = _StorageClass.defaultInstance
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "synchronization"
+
+extension Synchronization_Session: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".Session"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "identifier"),
+    2: .same(proto: "version"),
+    3: .same(proto: "creationTime"),
+    4: .same(proto: "creatingVersionMajor"),
+    5: .same(proto: "creatingVersionMinor"),
+    6: .same(proto: "creatingVersionPatch"),
+    7: .same(proto: "alpha"),
+    8: .same(proto: "beta"),
+    9: .same(proto: "configuration"),
+    11: .same(proto: "configurationAlpha"),
+    12: .same(proto: "configurationBeta"),
+    14: .same(proto: "name"),
+    13: .same(proto: "labels"),
+    10: .same(proto: "paused"),
+  ]
+
+  fileprivate class _StorageClass {
+    var _identifier: String = String()
+    var _version: Synchronization_Version = .invalid
+    var _creationTime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil
+    var _creatingVersionMajor: UInt32 = 0
+    var _creatingVersionMinor: UInt32 = 0
+    var _creatingVersionPatch: UInt32 = 0
+    var _alpha: Url_URL? = nil
+    var _beta: Url_URL? = nil
+    var _configuration: Synchronization_Configuration? = nil
+    var _configurationAlpha: Synchronization_Configuration? = nil
+    var _configurationBeta: Synchronization_Configuration? = nil
+    var _name: String = String()
+    var _labels: Dictionary<String,String> = [:]
+    var _paused: Bool = false
+
+    #if swift(>=5.10)
+      // This property is used as the initial default value for new instances of the type.
+      // The type itself is protecting the reference to its storage via CoW semantics.
+      // This will force a copy to be made of this reference when the first mutation occurs;
+      // hence, it is safe to mark this as `nonisolated(unsafe)`.
+      static nonisolated(unsafe) let defaultInstance = _StorageClass()
+    #else
+      static let defaultInstance = _StorageClass()
+    #endif
+
+    private init() {}
+
+    init(copying source: _StorageClass) {
+      _identifier = source._identifier
+      _version = source._version
+      _creationTime = source._creationTime
+      _creatingVersionMajor = source._creatingVersionMajor
+      _creatingVersionMinor = source._creatingVersionMinor
+      _creatingVersionPatch = source._creatingVersionPatch
+      _alpha = source._alpha
+      _beta = source._beta
+      _configuration = source._configuration
+      _configurationAlpha = source._configurationAlpha
+      _configurationBeta = source._configurationBeta
+      _name = source._name
+      _labels = source._labels
+      _paused = source._paused
+    }
+  }
+
+  fileprivate mutating func _uniqueStorage() -> _StorageClass {
+    if !isKnownUniquelyReferenced(&_storage) {
+      _storage = _StorageClass(copying: _storage)
+    }
+    return _storage
+  }
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    _ = _uniqueStorage()
+    try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
+      while let fieldNumber = try decoder.nextFieldNumber() {
+        // The use of inline closures is to circumvent an issue where the compiler
+        // allocates stack space for every case branch when no optimizations are
+        // enabled. https://github.com/apple/swift-protobuf/issues/1034
+        switch fieldNumber {
+        case 1: try { try decoder.decodeSingularStringField(value: &_storage._identifier) }()
+        case 2: try { try decoder.decodeSingularEnumField(value: &_storage._version) }()
+        case 3: try { try decoder.decodeSingularMessageField(value: &_storage._creationTime) }()
+        case 4: try { try decoder.decodeSingularUInt32Field(value: &_storage._creatingVersionMajor) }()
+        case 5: try { try decoder.decodeSingularUInt32Field(value: &_storage._creatingVersionMinor) }()
+        case 6: try { try decoder.decodeSingularUInt32Field(value: &_storage._creatingVersionPatch) }()
+        case 7: try { try decoder.decodeSingularMessageField(value: &_storage._alpha) }()
+        case 8: try { try decoder.decodeSingularMessageField(value: &_storage._beta) }()
+        case 9: try { try decoder.decodeSingularMessageField(value: &_storage._configuration) }()
+        case 10: try { try decoder.decodeSingularBoolField(value: &_storage._paused) }()
+        case 11: try { try decoder.decodeSingularMessageField(value: &_storage._configurationAlpha) }()
+        case 12: try { try decoder.decodeSingularMessageField(value: &_storage._configurationBeta) }()
+        case 13: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap<SwiftProtobuf.ProtobufString,SwiftProtobuf.ProtobufString>.self, value: &_storage._labels) }()
+        case 14: try { try decoder.decodeSingularStringField(value: &_storage._name) }()
+        default: break
+        }
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every if/case branch local when no optimizations
+      // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+      // https://github.com/apple/swift-protobuf/issues/1182
+      if !_storage._identifier.isEmpty {
+        try visitor.visitSingularStringField(value: _storage._identifier, fieldNumber: 1)
+      }
+      if _storage._version != .invalid {
+        try visitor.visitSingularEnumField(value: _storage._version, fieldNumber: 2)
+      }
+      try { if let v = _storage._creationTime {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
+      } }()
+      if _storage._creatingVersionMajor != 0 {
+        try visitor.visitSingularUInt32Field(value: _storage._creatingVersionMajor, fieldNumber: 4)
+      }
+      if _storage._creatingVersionMinor != 0 {
+        try visitor.visitSingularUInt32Field(value: _storage._creatingVersionMinor, fieldNumber: 5)
+      }
+      if _storage._creatingVersionPatch != 0 {
+        try visitor.visitSingularUInt32Field(value: _storage._creatingVersionPatch, fieldNumber: 6)
+      }
+      try { if let v = _storage._alpha {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
+      } }()
+      try { if let v = _storage._beta {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
+      } }()
+      try { if let v = _storage._configuration {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
+      } }()
+      if _storage._paused != false {
+        try visitor.visitSingularBoolField(value: _storage._paused, fieldNumber: 10)
+      }
+      try { if let v = _storage._configurationAlpha {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 11)
+      } }()
+      try { if let v = _storage._configurationBeta {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
+      } }()
+      if !_storage._labels.isEmpty {
+        try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap<SwiftProtobuf.ProtobufString,SwiftProtobuf.ProtobufString>.self, value: _storage._labels, fieldNumber: 13)
+      }
+      if !_storage._name.isEmpty {
+        try visitor.visitSingularStringField(value: _storage._name, fieldNumber: 14)
+      }
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_Session, rhs: Synchronization_Session) -> Bool {
+    if lhs._storage !== rhs._storage {
+      let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
+        let _storage = _args.0
+        let rhs_storage = _args.1
+        if _storage._identifier != rhs_storage._identifier {return false}
+        if _storage._version != rhs_storage._version {return false}
+        if _storage._creationTime != rhs_storage._creationTime {return false}
+        if _storage._creatingVersionMajor != rhs_storage._creatingVersionMajor {return false}
+        if _storage._creatingVersionMinor != rhs_storage._creatingVersionMinor {return false}
+        if _storage._creatingVersionPatch != rhs_storage._creatingVersionPatch {return false}
+        if _storage._alpha != rhs_storage._alpha {return false}
+        if _storage._beta != rhs_storage._beta {return false}
+        if _storage._configuration != rhs_storage._configuration {return false}
+        if _storage._configurationAlpha != rhs_storage._configurationAlpha {return false}
+        if _storage._configurationBeta != rhs_storage._configurationBeta {return false}
+        if _storage._name != rhs_storage._name {return false}
+        if _storage._labels != rhs_storage._labels {return false}
+        if _storage._paused != rhs_storage._paused {return false}
+        return true
+      }
+      if !storagesAreEqual {return false}
+    }
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_session.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_session.proto
new file mode 100644
index 00000000..9f3f1659
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_session.proto
@@ -0,0 +1,100 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/session.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package synchronization;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization";
+
+import "google/protobuf/timestamp.proto";
+
+import "synchronization_configuration.proto";
+import "synchronization_version.proto";
+import "url_url.proto";
+
+// Session represents a synchronization session configuration and persistent
+// state. It is mutable within the context of the daemon, so it should be
+// accessed and modified in a synchronized fashion. Outside of the daemon (e.g.
+// when returned via the API), it should be considered immutable.
+message Session {
+    // The identifier, version, creationTime, and creatingVersion* fields are
+    // considered the "header" fields for all session versions. A message
+    // composed purely of these fields is guaranteed to be compatible with all
+    // future session versions. This can be used to dispatch session decoding to
+    // more specific message structures once multiple session version formats
+    // are implemented.
+
+    // Identifier is the (unique) session identifier. It is static. It cannot be
+    // empty.
+    string identifier = 1;
+    // Version is the session version. It is static.
+    Version version = 2;
+    // CreationTime is the creation time of the session. It is static. It cannot
+    // be nil.
+    google.protobuf.Timestamp creationTime = 3;
+    // CreatingVersionMajor is the major version component of the version of
+    // Mutagen which created the session. It is static.
+    uint32 creatingVersionMajor = 4;
+    // CreatingVersionMinor is the minor version component of the version of
+    // Mutagen which created the session. It is static.
+    uint32 creatingVersionMinor = 5;
+    // CreatingVersionPatch is the patch version component of the version of
+    // Mutagen which created the session. It is static.
+    uint32 creatingVersionPatch = 6;
+
+    // The remaining fields are those currently used by session version 1.
+
+    // Alpha is the alpha endpoint URL. It is static. It cannot be nil.
+    url.URL alpha = 7;
+    // Beta is the beta endpoint URL. It is static. It cannot be nil.
+    url.URL beta = 8;
+    // Configuration is the flattened session configuration. It is static. It
+    // cannot be nil.
+    Configuration configuration = 9;
+    // ConfigurationAlpha are the alpha-specific session configuration
+    // overrides. It is static. It may be nil for existing sessions loaded from
+    // disk, but it is not considered valid unless non-nil, so it should be
+    // replaced with an empty default value in-memory if a nil on-disk value is
+    // detected.
+    Configuration configurationAlpha = 11;
+    // ConfigurationBeta are the beta-specific session configuration overrides.
+    // It is static. It may be nil for existing sessions loaded from disk, but
+    // it is not considered valid unless non-nil, so it should be replaced with
+    // an empty default value in-memory if a nil on-disk value is detected.
+    Configuration configurationBeta = 12;
+    // Name is a user-friendly name for the session. It may be empty and is not
+    // guaranteed to be unique across all sessions. It is only used as a simpler
+    // handle for specifying sessions. It is static.
+    string name = 14;
+    // Labels are the session labels. They are static.
+    map<string, string> labels = 13;
+    // Paused indicates whether or not the session is marked as paused.
+    bool paused = 10;
+    // NOTE: Fields 11, 12, 13, and 14 are used above. They are out of order for
+    // historical reasons.
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_stage_mode.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_stage_mode.pb.swift
new file mode 100644
index 00000000..61769ace
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_stage_mode.pb.swift
@@ -0,0 +1,115 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_stage_mode.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/stage_mode.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// StageMode specifies the mode for file staging.
+enum Synchronization_StageMode: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// StageMode_StageModeDefault represents an unspecified staging mode. It
+  /// should be converted to one of the following values based on the desired
+  /// default behavior.
+  case `default` // = 0
+
+  /// StageMode_StageModeMutagen specifies that files should be staged in the
+  /// Mutagen data directory.
+  case mutagen // = 1
+
+  /// StageMode_StageModeNeighboring specifies that files should be staged in a
+  /// directory which neighbors the synchronization root.
+  case neighboring // = 2
+
+  /// StageMode_StageModeInternal specified that files should be staged in a
+  /// directory contained within a synchronization root. This mode will only
+  /// function if the synchronization root already exists.
+  case `internal` // = 3
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .mutagen
+    case 2: self = .neighboring
+    case 3: self = .internal
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .mutagen: return 1
+    case .neighboring: return 2
+    case .internal: return 3
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Synchronization_StageMode] = [
+    .default,
+    .mutagen,
+    .neighboring,
+    .internal,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Synchronization_StageMode: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "StageModeDefault"),
+    1: .same(proto: "StageModeMutagen"),
+    2: .same(proto: "StageModeNeighboring"),
+    3: .same(proto: "StageModeInternal"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_stage_mode.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_stage_mode.proto
new file mode 100644
index 00000000..f049b9a5
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_stage_mode.proto
@@ -0,0 +1,50 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/stage_mode.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package synchronization;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization";
+
+// StageMode specifies the mode for file staging.
+enum StageMode {
+    // StageMode_StageModeDefault represents an unspecified staging mode. It
+    // should be converted to one of the following values based on the desired
+    // default behavior.
+    StageModeDefault = 0;
+    // StageMode_StageModeMutagen specifies that files should be staged in the
+    // Mutagen data directory.
+    StageModeMutagen = 1;
+    // StageMode_StageModeNeighboring specifies that files should be staged in a
+    // directory which neighbors the synchronization root.
+    StageModeNeighboring = 2;
+    // StageMode_StageModeInternal specified that files should be staged in a
+    // directory contained within a synchronization root. This mode will only
+    // function if the synchronization root already exists.
+    StageModeInternal = 3;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_state.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_state.pb.swift
new file mode 100644
index 00000000..0d7ef6cf
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_state.pb.swift
@@ -0,0 +1,579 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_state.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/state.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Status encodes the status of a synchronization session.
+enum Synchronization_Status: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// Status_Disconnected indicates that the session is unpaused but not
+  /// currently connected or connecting to either endpoint.
+  case disconnected // = 0
+
+  /// Status_HaltedOnRootEmptied indicates that the session is halted due to
+  /// the root emptying safety check.
+  case haltedOnRootEmptied // = 1
+
+  /// Status_HaltedOnRootDeletion indicates that the session is halted due to
+  /// the root deletion safety check.
+  case haltedOnRootDeletion // = 2
+
+  /// Status_HaltedOnRootTypeChange indicates that the session is halted due to
+  /// the root type change safety check.
+  case haltedOnRootTypeChange // = 3
+
+  /// Status_ConnectingAlpha indicates that the session is attempting to
+  /// connect to the alpha endpoint.
+  case connectingAlpha // = 4
+
+  /// Status_ConnectingBeta indicates that the session is attempting to connect
+  /// to the beta endpoint.
+  case connectingBeta // = 5
+
+  /// Status_Watching indicates that the session is watching for filesystem
+  /// changes.
+  case watching // = 6
+
+  /// Status_Scanning indicates that the session is scanning the filesystem on
+  /// each endpoint.
+  case scanning // = 7
+
+  /// Status_WaitingForRescan indicates that the session is waiting to retry
+  /// scanning after an error during the previous scanning operation.
+  case waitingForRescan // = 8
+
+  /// Status_Reconciling indicates that the session is performing
+  /// reconciliation.
+  case reconciling // = 9
+
+  /// Status_StagingAlpha indicates that the session is staging files on alpha.
+  case stagingAlpha // = 10
+
+  /// Status_StagingBeta indicates that the session is staging files on beta.
+  case stagingBeta // = 11
+
+  /// Status_Transitioning indicates that the session is performing transition
+  /// operations on each endpoint.
+  case transitioning // = 12
+
+  /// Status_Saving indicates that the session is recording synchronization
+  /// history to disk.
+  case saving // = 13
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .disconnected
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .disconnected
+    case 1: self = .haltedOnRootEmptied
+    case 2: self = .haltedOnRootDeletion
+    case 3: self = .haltedOnRootTypeChange
+    case 4: self = .connectingAlpha
+    case 5: self = .connectingBeta
+    case 6: self = .watching
+    case 7: self = .scanning
+    case 8: self = .waitingForRescan
+    case 9: self = .reconciling
+    case 10: self = .stagingAlpha
+    case 11: self = .stagingBeta
+    case 12: self = .transitioning
+    case 13: self = .saving
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .disconnected: return 0
+    case .haltedOnRootEmptied: return 1
+    case .haltedOnRootDeletion: return 2
+    case .haltedOnRootTypeChange: return 3
+    case .connectingAlpha: return 4
+    case .connectingBeta: return 5
+    case .watching: return 6
+    case .scanning: return 7
+    case .waitingForRescan: return 8
+    case .reconciling: return 9
+    case .stagingAlpha: return 10
+    case .stagingBeta: return 11
+    case .transitioning: return 12
+    case .saving: return 13
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Synchronization_Status] = [
+    .disconnected,
+    .haltedOnRootEmptied,
+    .haltedOnRootDeletion,
+    .haltedOnRootTypeChange,
+    .connectingAlpha,
+    .connectingBeta,
+    .watching,
+    .scanning,
+    .waitingForRescan,
+    .reconciling,
+    .stagingAlpha,
+    .stagingBeta,
+    .transitioning,
+    .saving,
+  ]
+
+}
+
+/// EndpointState encodes the current state of a synchronization endpoint. It is
+/// mutable within the context of the daemon, so it should be accessed and
+/// modified in a synchronized fashion. Outside of the daemon (e.g. when returned
+/// via the API), it should be considered immutable.
+struct Synchronization_EndpointState: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Connected indicates whether or not the controller is currently connected
+  /// to the endpoint.
+  var connected: Bool = false
+
+  /// Scanned indicates whether or not at least one scan has been performed on
+  /// the endpoint.
+  var scanned: Bool = false
+
+  /// Directories is the number of synchronizable directory entries contained
+  /// in the last snapshot from the endpoint.
+  var directories: UInt64 = 0
+
+  /// Files is the number of synchronizable file entries contained in the last
+  /// snapshot from the endpoint.
+  var files: UInt64 = 0
+
+  /// SymbolicLinks is the number of synchronizable symbolic link entries
+  /// contained in the last snapshot from the endpoint.
+  var symbolicLinks: UInt64 = 0
+
+  /// TotalFileSize is the total size of all synchronizable files referenced by
+  /// the last snapshot from the endpoint.
+  var totalFileSize: UInt64 = 0
+
+  /// ScanProblems is the list of non-terminal problems encountered during the
+  /// last scanning operation on the endpoint. This list may be a truncated
+  /// version of the full list if too many problems are encountered to report
+  /// via the API, in which case ExcludedScanProblems will be non-zero.
+  var scanProblems: [Core_Problem] = []
+
+  /// ExcludedScanProblems is the number of problems that have been excluded
+  /// from ScanProblems due to truncation. This value can be non-zero only if
+  /// ScanProblems is non-empty.
+  var excludedScanProblems: UInt64 = 0
+
+  /// TransitionProblems is the list of non-terminal problems encountered
+  /// during the last transition operation on the endpoint. This list may be a
+  /// truncated version of the full list if too many problems are encountered
+  /// to report via the API, in which case ExcludedTransitionProblems will be
+  /// non-zero.
+  var transitionProblems: [Core_Problem] = []
+
+  /// ExcludedTransitionProblems is the number of problems that have been
+  /// excluded from TransitionProblems due to truncation. This value can be
+  /// non-zero only if TransitionProblems is non-empty.
+  var excludedTransitionProblems: UInt64 = 0
+
+  /// StagingProgress is the rsync staging progress. It is non-nil if and only
+  /// if the endpoint is currently staging files.
+  var stagingProgress: Rsync_ReceiverState {
+    get {return _stagingProgress ?? Rsync_ReceiverState()}
+    set {_stagingProgress = newValue}
+  }
+  /// Returns true if `stagingProgress` has been explicitly set.
+  var hasStagingProgress: Bool {return self._stagingProgress != nil}
+  /// Clears the value of `stagingProgress`. Subsequent reads from it will return its default value.
+  mutating func clearStagingProgress() {self._stagingProgress = nil}
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _stagingProgress: Rsync_ReceiverState? = nil
+}
+
+/// State encodes the current state of a synchronization session. It is mutable
+/// within the context of the daemon, so it should be accessed and modified in a
+/// synchronized fashion. Outside of the daemon (e.g. when returned via the API),
+/// it should be considered immutable.
+struct Synchronization_State: @unchecked Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Session is the session metadata. If the session is paused, then the
+  /// remainder of the fields in this structure should be ignored.
+  var session: Synchronization_Session {
+    get {return _storage._session ?? Synchronization_Session()}
+    set {_uniqueStorage()._session = newValue}
+  }
+  /// Returns true if `session` has been explicitly set.
+  var hasSession: Bool {return _storage._session != nil}
+  /// Clears the value of `session`. Subsequent reads from it will return its default value.
+  mutating func clearSession() {_uniqueStorage()._session = nil}
+
+  /// Status is the session status.
+  var status: Synchronization_Status {
+    get {return _storage._status}
+    set {_uniqueStorage()._status = newValue}
+  }
+
+  /// LastError is the last error to occur during synchronization. It is
+  /// cleared after a successful synchronization cycle.
+  var lastError: String {
+    get {return _storage._lastError}
+    set {_uniqueStorage()._lastError = newValue}
+  }
+
+  /// SuccessfulCycles is the number of successful synchronization cycles to
+  /// occur since successfully connecting to the endpoints.
+  var successfulCycles: UInt64 {
+    get {return _storage._successfulCycles}
+    set {_uniqueStorage()._successfulCycles = newValue}
+  }
+
+  /// Conflicts are the content conflicts identified during reconciliation.
+  /// This list may be a truncated version of the full list if too many
+  /// conflicts are encountered to report via the API, in which case
+  /// ExcludedConflicts will be non-zero.
+  var conflicts: [Core_Conflict] {
+    get {return _storage._conflicts}
+    set {_uniqueStorage()._conflicts = newValue}
+  }
+
+  /// ExcludedConflicts is the number of conflicts that have been excluded from
+  /// Conflicts due to truncation. This value can be non-zero only if conflicts
+  /// is non-empty.
+  var excludedConflicts: UInt64 {
+    get {return _storage._excludedConflicts}
+    set {_uniqueStorage()._excludedConflicts = newValue}
+  }
+
+  /// AlphaState encodes the state of the alpha endpoint. It is always non-nil.
+  var alphaState: Synchronization_EndpointState {
+    get {return _storage._alphaState ?? Synchronization_EndpointState()}
+    set {_uniqueStorage()._alphaState = newValue}
+  }
+  /// Returns true if `alphaState` has been explicitly set.
+  var hasAlphaState: Bool {return _storage._alphaState != nil}
+  /// Clears the value of `alphaState`. Subsequent reads from it will return its default value.
+  mutating func clearAlphaState() {_uniqueStorage()._alphaState = nil}
+
+  /// BetaState encodes the state of the beta endpoint. It is always non-nil.
+  var betaState: Synchronization_EndpointState {
+    get {return _storage._betaState ?? Synchronization_EndpointState()}
+    set {_uniqueStorage()._betaState = newValue}
+  }
+  /// Returns true if `betaState` has been explicitly set.
+  var hasBetaState: Bool {return _storage._betaState != nil}
+  /// Clears the value of `betaState`. Subsequent reads from it will return its default value.
+  mutating func clearBetaState() {_uniqueStorage()._betaState = nil}
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+
+  fileprivate var _storage = _StorageClass.defaultInstance
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "synchronization"
+
+extension Synchronization_Status: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "Disconnected"),
+    1: .same(proto: "HaltedOnRootEmptied"),
+    2: .same(proto: "HaltedOnRootDeletion"),
+    3: .same(proto: "HaltedOnRootTypeChange"),
+    4: .same(proto: "ConnectingAlpha"),
+    5: .same(proto: "ConnectingBeta"),
+    6: .same(proto: "Watching"),
+    7: .same(proto: "Scanning"),
+    8: .same(proto: "WaitingForRescan"),
+    9: .same(proto: "Reconciling"),
+    10: .same(proto: "StagingAlpha"),
+    11: .same(proto: "StagingBeta"),
+    12: .same(proto: "Transitioning"),
+    13: .same(proto: "Saving"),
+  ]
+}
+
+extension Synchronization_EndpointState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".EndpointState"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "connected"),
+    2: .same(proto: "scanned"),
+    3: .same(proto: "directories"),
+    4: .same(proto: "files"),
+    5: .same(proto: "symbolicLinks"),
+    6: .same(proto: "totalFileSize"),
+    7: .same(proto: "scanProblems"),
+    8: .same(proto: "excludedScanProblems"),
+    9: .same(proto: "transitionProblems"),
+    10: .same(proto: "excludedTransitionProblems"),
+    11: .same(proto: "stagingProgress"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularBoolField(value: &self.connected) }()
+      case 2: try { try decoder.decodeSingularBoolField(value: &self.scanned) }()
+      case 3: try { try decoder.decodeSingularUInt64Field(value: &self.directories) }()
+      case 4: try { try decoder.decodeSingularUInt64Field(value: &self.files) }()
+      case 5: try { try decoder.decodeSingularUInt64Field(value: &self.symbolicLinks) }()
+      case 6: try { try decoder.decodeSingularUInt64Field(value: &self.totalFileSize) }()
+      case 7: try { try decoder.decodeRepeatedMessageField(value: &self.scanProblems) }()
+      case 8: try { try decoder.decodeSingularUInt64Field(value: &self.excludedScanProblems) }()
+      case 9: try { try decoder.decodeRepeatedMessageField(value: &self.transitionProblems) }()
+      case 10: try { try decoder.decodeSingularUInt64Field(value: &self.excludedTransitionProblems) }()
+      case 11: try { try decoder.decodeSingularMessageField(value: &self._stagingProgress) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    if self.connected != false {
+      try visitor.visitSingularBoolField(value: self.connected, fieldNumber: 1)
+    }
+    if self.scanned != false {
+      try visitor.visitSingularBoolField(value: self.scanned, fieldNumber: 2)
+    }
+    if self.directories != 0 {
+      try visitor.visitSingularUInt64Field(value: self.directories, fieldNumber: 3)
+    }
+    if self.files != 0 {
+      try visitor.visitSingularUInt64Field(value: self.files, fieldNumber: 4)
+    }
+    if self.symbolicLinks != 0 {
+      try visitor.visitSingularUInt64Field(value: self.symbolicLinks, fieldNumber: 5)
+    }
+    if self.totalFileSize != 0 {
+      try visitor.visitSingularUInt64Field(value: self.totalFileSize, fieldNumber: 6)
+    }
+    if !self.scanProblems.isEmpty {
+      try visitor.visitRepeatedMessageField(value: self.scanProblems, fieldNumber: 7)
+    }
+    if self.excludedScanProblems != 0 {
+      try visitor.visitSingularUInt64Field(value: self.excludedScanProblems, fieldNumber: 8)
+    }
+    if !self.transitionProblems.isEmpty {
+      try visitor.visitRepeatedMessageField(value: self.transitionProblems, fieldNumber: 9)
+    }
+    if self.excludedTransitionProblems != 0 {
+      try visitor.visitSingularUInt64Field(value: self.excludedTransitionProblems, fieldNumber: 10)
+    }
+    try { if let v = self._stagingProgress {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 11)
+    } }()
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_EndpointState, rhs: Synchronization_EndpointState) -> Bool {
+    if lhs.connected != rhs.connected {return false}
+    if lhs.scanned != rhs.scanned {return false}
+    if lhs.directories != rhs.directories {return false}
+    if lhs.files != rhs.files {return false}
+    if lhs.symbolicLinks != rhs.symbolicLinks {return false}
+    if lhs.totalFileSize != rhs.totalFileSize {return false}
+    if lhs.scanProblems != rhs.scanProblems {return false}
+    if lhs.excludedScanProblems != rhs.excludedScanProblems {return false}
+    if lhs.transitionProblems != rhs.transitionProblems {return false}
+    if lhs.excludedTransitionProblems != rhs.excludedTransitionProblems {return false}
+    if lhs._stagingProgress != rhs._stagingProgress {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Synchronization_State: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".State"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "session"),
+    2: .same(proto: "status"),
+    3: .same(proto: "lastError"),
+    4: .same(proto: "successfulCycles"),
+    5: .same(proto: "conflicts"),
+    6: .same(proto: "excludedConflicts"),
+    7: .same(proto: "alphaState"),
+    8: .same(proto: "betaState"),
+  ]
+
+  fileprivate class _StorageClass {
+    var _session: Synchronization_Session? = nil
+    var _status: Synchronization_Status = .disconnected
+    var _lastError: String = String()
+    var _successfulCycles: UInt64 = 0
+    var _conflicts: [Core_Conflict] = []
+    var _excludedConflicts: UInt64 = 0
+    var _alphaState: Synchronization_EndpointState? = nil
+    var _betaState: Synchronization_EndpointState? = nil
+
+    #if swift(>=5.10)
+      // This property is used as the initial default value for new instances of the type.
+      // The type itself is protecting the reference to its storage via CoW semantics.
+      // This will force a copy to be made of this reference when the first mutation occurs;
+      // hence, it is safe to mark this as `nonisolated(unsafe)`.
+      static nonisolated(unsafe) let defaultInstance = _StorageClass()
+    #else
+      static let defaultInstance = _StorageClass()
+    #endif
+
+    private init() {}
+
+    init(copying source: _StorageClass) {
+      _session = source._session
+      _status = source._status
+      _lastError = source._lastError
+      _successfulCycles = source._successfulCycles
+      _conflicts = source._conflicts
+      _excludedConflicts = source._excludedConflicts
+      _alphaState = source._alphaState
+      _betaState = source._betaState
+    }
+  }
+
+  fileprivate mutating func _uniqueStorage() -> _StorageClass {
+    if !isKnownUniquelyReferenced(&_storage) {
+      _storage = _StorageClass(copying: _storage)
+    }
+    return _storage
+  }
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    _ = _uniqueStorage()
+    try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
+      while let fieldNumber = try decoder.nextFieldNumber() {
+        // The use of inline closures is to circumvent an issue where the compiler
+        // allocates stack space for every case branch when no optimizations are
+        // enabled. https://github.com/apple/swift-protobuf/issues/1034
+        switch fieldNumber {
+        case 1: try { try decoder.decodeSingularMessageField(value: &_storage._session) }()
+        case 2: try { try decoder.decodeSingularEnumField(value: &_storage._status) }()
+        case 3: try { try decoder.decodeSingularStringField(value: &_storage._lastError) }()
+        case 4: try { try decoder.decodeSingularUInt64Field(value: &_storage._successfulCycles) }()
+        case 5: try { try decoder.decodeRepeatedMessageField(value: &_storage._conflicts) }()
+        case 6: try { try decoder.decodeSingularUInt64Field(value: &_storage._excludedConflicts) }()
+        case 7: try { try decoder.decodeSingularMessageField(value: &_storage._alphaState) }()
+        case 8: try { try decoder.decodeSingularMessageField(value: &_storage._betaState) }()
+        default: break
+        }
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every if/case branch local when no optimizations
+      // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+      // https://github.com/apple/swift-protobuf/issues/1182
+      try { if let v = _storage._session {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
+      } }()
+      if _storage._status != .disconnected {
+        try visitor.visitSingularEnumField(value: _storage._status, fieldNumber: 2)
+      }
+      if !_storage._lastError.isEmpty {
+        try visitor.visitSingularStringField(value: _storage._lastError, fieldNumber: 3)
+      }
+      if _storage._successfulCycles != 0 {
+        try visitor.visitSingularUInt64Field(value: _storage._successfulCycles, fieldNumber: 4)
+      }
+      if !_storage._conflicts.isEmpty {
+        try visitor.visitRepeatedMessageField(value: _storage._conflicts, fieldNumber: 5)
+      }
+      if _storage._excludedConflicts != 0 {
+        try visitor.visitSingularUInt64Field(value: _storage._excludedConflicts, fieldNumber: 6)
+      }
+      try { if let v = _storage._alphaState {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
+      } }()
+      try { if let v = _storage._betaState {
+        try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
+      } }()
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Synchronization_State, rhs: Synchronization_State) -> Bool {
+    if lhs._storage !== rhs._storage {
+      let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
+        let _storage = _args.0
+        let rhs_storage = _args.1
+        if _storage._session != rhs_storage._session {return false}
+        if _storage._status != rhs_storage._status {return false}
+        if _storage._lastError != rhs_storage._lastError {return false}
+        if _storage._successfulCycles != rhs_storage._successfulCycles {return false}
+        if _storage._conflicts != rhs_storage._conflicts {return false}
+        if _storage._excludedConflicts != rhs_storage._excludedConflicts {return false}
+        if _storage._alphaState != rhs_storage._alphaState {return false}
+        if _storage._betaState != rhs_storage._betaState {return false}
+        return true
+      }
+      if !storagesAreEqual {return false}
+    }
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_state.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_state.proto
new file mode 100644
index 00000000..78c918dc
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_state.proto
@@ -0,0 +1,159 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/state.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package synchronization;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization";
+
+import "synchronization_rsync_receive.proto";
+import "synchronization_session.proto";
+import "synchronization_core_conflict.proto";
+import "synchronization_core_problem.proto";
+
+// Status encodes the status of a synchronization session.
+enum Status {
+    // Status_Disconnected indicates that the session is unpaused but not
+    // currently connected or connecting to either endpoint.
+    Disconnected = 0;
+    // Status_HaltedOnRootEmptied indicates that the session is halted due to
+    // the root emptying safety check.
+    HaltedOnRootEmptied = 1;
+    // Status_HaltedOnRootDeletion indicates that the session is halted due to
+    // the root deletion safety check.
+    HaltedOnRootDeletion = 2;
+    // Status_HaltedOnRootTypeChange indicates that the session is halted due to
+    // the root type change safety check.
+    HaltedOnRootTypeChange = 3;
+    // Status_ConnectingAlpha indicates that the session is attempting to
+    // connect to the alpha endpoint.
+    ConnectingAlpha = 4;
+    // Status_ConnectingBeta indicates that the session is attempting to connect
+    // to the beta endpoint.
+    ConnectingBeta = 5;
+    // Status_Watching indicates that the session is watching for filesystem
+    // changes.
+    Watching = 6;
+    // Status_Scanning indicates that the session is scanning the filesystem on
+    // each endpoint.
+    Scanning = 7;
+    // Status_WaitingForRescan indicates that the session is waiting to retry
+    // scanning after an error during the previous scanning operation.
+    WaitingForRescan = 8;
+    // Status_Reconciling indicates that the session is performing
+    // reconciliation.
+    Reconciling = 9;
+    // Status_StagingAlpha indicates that the session is staging files on alpha.
+    StagingAlpha = 10;
+    // Status_StagingBeta indicates that the session is staging files on beta.
+    StagingBeta = 11;
+    // Status_Transitioning indicates that the session is performing transition
+    // operations on each endpoint.
+    Transitioning = 12;
+    // Status_Saving indicates that the session is recording synchronization
+    // history to disk.
+    Saving = 13;
+}
+
+// EndpointState encodes the current state of a synchronization endpoint. It is
+// mutable within the context of the daemon, so it should be accessed and
+// modified in a synchronized fashion. Outside of the daemon (e.g. when returned
+// via the API), it should be considered immutable.
+message EndpointState {
+    // Connected indicates whether or not the controller is currently connected
+    // to the endpoint.
+    bool connected = 1;
+    // Scanned indicates whether or not at least one scan has been performed on
+    // the endpoint.
+    bool scanned = 2;
+    // Directories is the number of synchronizable directory entries contained
+    // in the last snapshot from the endpoint.
+    uint64 directories = 3;
+    // Files is the number of synchronizable file entries contained in the last
+    // snapshot from the endpoint.
+    uint64 files = 4;
+    // SymbolicLinks is the number of synchronizable symbolic link entries
+    // contained in the last snapshot from the endpoint.
+    uint64 symbolicLinks = 5;
+    // TotalFileSize is the total size of all synchronizable files referenced by
+    // the last snapshot from the endpoint.
+    uint64 totalFileSize = 6;
+    // ScanProblems is the list of non-terminal problems encountered during the
+    // last scanning operation on the endpoint. This list may be a truncated
+    // version of the full list if too many problems are encountered to report
+    // via the API, in which case ExcludedScanProblems will be non-zero.
+    repeated core.Problem scanProblems = 7;
+    // ExcludedScanProblems is the number of problems that have been excluded
+    // from ScanProblems due to truncation. This value can be non-zero only if
+    // ScanProblems is non-empty.
+    uint64 excludedScanProblems = 8;
+    // TransitionProblems is the list of non-terminal problems encountered
+    // during the last transition operation on the endpoint. This list may be a
+    // truncated version of the full list if too many problems are encountered
+    // to report via the API, in which case ExcludedTransitionProblems will be
+    // non-zero.
+    repeated core.Problem transitionProblems = 9;
+    // ExcludedTransitionProblems is the number of problems that have been
+    // excluded from TransitionProblems due to truncation. This value can be
+    // non-zero only if TransitionProblems is non-empty.
+    uint64 excludedTransitionProblems = 10;
+    // StagingProgress is the rsync staging progress. It is non-nil if and only
+    // if the endpoint is currently staging files.
+    rsync.ReceiverState stagingProgress = 11;
+}
+
+// State encodes the current state of a synchronization session. It is mutable
+// within the context of the daemon, so it should be accessed and modified in a
+// synchronized fashion. Outside of the daemon (e.g. when returned via the API),
+// it should be considered immutable.
+message State {
+    // Session is the session metadata. If the session is paused, then the
+    // remainder of the fields in this structure should be ignored.
+    Session session = 1;
+    // Status is the session status.
+    Status status = 2;
+    // LastError is the last error to occur during synchronization. It is
+    // cleared after a successful synchronization cycle.
+    string lastError = 3;
+    // SuccessfulCycles is the number of successful synchronization cycles to
+    // occur since successfully connecting to the endpoints.
+    uint64 successfulCycles = 4;
+    // Conflicts are the content conflicts identified during reconciliation.
+    // This list may be a truncated version of the full list if too many
+    // conflicts are encountered to report via the API, in which case
+    // ExcludedConflicts will be non-zero.
+    repeated core.Conflict conflicts = 5;
+    // ExcludedConflicts is the number of conflicts that have been excluded from
+    // Conflicts due to truncation. This value can be non-zero only if conflicts
+    // is non-empty.
+    uint64 excludedConflicts = 6;
+    // AlphaState encodes the state of the alpha endpoint. It is always non-nil.
+    EndpointState alphaState = 7;
+    // BetaState encodes the state of the beta endpoint. It is always non-nil.
+    EndpointState betaState = 8;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_version.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_version.pb.swift
new file mode 100644
index 00000000..d62b116e
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_version.pb.swift
@@ -0,0 +1,98 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_version.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/version.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Version specifies a session version, providing default behavior that can vary
+/// without affecting existing sessions.
+enum Synchronization_Version: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// Invalid is the default session version and represents an unspecfied and
+  /// invalid version. It is used as a sanity check to ensure that version is
+  /// set for a session.
+  case invalid // = 0
+
+  /// Version1 represents session version 1.
+  case version1 // = 1
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .invalid
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .invalid
+    case 1: self = .version1
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .invalid: return 0
+    case .version1: return 1
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Synchronization_Version] = [
+    .invalid,
+    .version1,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Synchronization_Version: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "Invalid"),
+    1: .same(proto: "Version1"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_version.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_version.proto
new file mode 100644
index 00000000..9c5c2962
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_version.proto
@@ -0,0 +1,43 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/version.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package synchronization;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization";
+
+// Version specifies a session version, providing default behavior that can vary
+// without affecting existing sessions.
+enum Version {
+    // Invalid is the default session version and represents an unspecfied and
+    // invalid version. It is used as a sanity check to ensure that version is
+    // set for a session.
+    Invalid = 0;
+    // Version1 represents session version 1.
+    Version1 = 1;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_watch_mode.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_watch_mode.pb.swift
new file mode 100644
index 00000000..7836b35d
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_watch_mode.pb.swift
@@ -0,0 +1,118 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: synchronization_watch_mode.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/watch_mode.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// WatchMode specifies the mode for filesystem watching.
+enum Synchronization_WatchMode: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// WatchMode_WatchModeDefault represents an unspecified watch mode. It
+  /// should be converted to one of the following values based on the desired
+  /// default behavior.
+  case `default` // = 0
+
+  /// WatchMode_WatchModePortable specifies that native recursive watching
+  /// should be used to monitor paths on systems that support it if those paths
+  /// fall under the home directory. In these cases, a watch on the entire home
+  /// directory is established and filtered for events pertaining to the
+  /// specified path. On all other systems and for all other paths, poll-based
+  /// watching is used.
+  case portable // = 1
+
+  /// WatchMode_WatchModeForcePoll specifies that only poll-based watching
+  /// should be used.
+  case forcePoll // = 2
+
+  /// WatchMode_WatchModeNoWatch specifies that no watching should be used
+  /// (i.e. no events should be generated).
+  case noWatch // = 3
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .default
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .default
+    case 1: self = .portable
+    case 2: self = .forcePoll
+    case 3: self = .noWatch
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .default: return 0
+    case .portable: return 1
+    case .forcePoll: return 2
+    case .noWatch: return 3
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Synchronization_WatchMode] = [
+    .default,
+    .portable,
+    .forcePoll,
+    .noWatch,
+  ]
+
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+extension Synchronization_WatchMode: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "WatchModeDefault"),
+    1: .same(proto: "WatchModePortable"),
+    2: .same(proto: "WatchModeForcePoll"),
+    3: .same(proto: "WatchModeNoWatch"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_watch_mode.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_watch_mode.proto
new file mode 100644
index 00000000..1fedd86f
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/synchronization_watch_mode.proto
@@ -0,0 +1,53 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/synchronization/watch_mode.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package synchronization;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/synchronization";
+
+// WatchMode specifies the mode for filesystem watching.
+enum WatchMode {
+    // WatchMode_WatchModeDefault represents an unspecified watch mode. It
+    // should be converted to one of the following values based on the desired
+    // default behavior.
+    WatchModeDefault = 0;
+    // WatchMode_WatchModePortable specifies that native recursive watching
+    // should be used to monitor paths on systems that support it if those paths
+    // fall under the home directory. In these cases, a watch on the entire home
+    // directory is established and filtered for events pertaining to the
+    // specified path. On all other systems and for all other paths, poll-based
+    // watching is used.
+    WatchModePortable = 1;
+    // WatchMode_WatchModeForcePoll specifies that only poll-based watching
+    // should be used.
+    WatchModeForcePoll = 2;
+    // WatchMode_WatchModeNoWatch specifies that no watching should be used
+    // (i.e. no events should be generated).
+    WatchModeNoWatch = 3;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/url_url.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/url_url.pb.swift
new file mode 100644
index 00000000..32a305e0
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/url_url.pb.swift
@@ -0,0 +1,266 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: url_url.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/url/url.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// Kind indicates the kind of a URL.
+enum Url_Kind: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// Synchronization indicates a synchronization URL.
+  case synchronization // = 0
+
+  /// Forwarding indicates a forwarding URL.
+  case forwarding // = 1
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .synchronization
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .synchronization
+    case 1: self = .forwarding
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .synchronization: return 0
+    case .forwarding: return 1
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Url_Kind] = [
+    .synchronization,
+    .forwarding,
+  ]
+
+}
+
+/// Protocol indicates a location type.
+enum Url_Protocol: SwiftProtobuf.Enum, Swift.CaseIterable {
+  typealias RawValue = Int
+
+  /// Local indicates that the resource is on the local system.
+  case local // = 0
+
+  /// SSH indicates that the resource is accessible via SSH.
+  case ssh // = 1
+
+  /// Docker indicates that the resource is inside a Docker container.
+  case docker // = 11
+  case UNRECOGNIZED(Int)
+
+  init() {
+    self = .local
+  }
+
+  init?(rawValue: Int) {
+    switch rawValue {
+    case 0: self = .local
+    case 1: self = .ssh
+    case 11: self = .docker
+    default: self = .UNRECOGNIZED(rawValue)
+    }
+  }
+
+  var rawValue: Int {
+    switch self {
+    case .local: return 0
+    case .ssh: return 1
+    case .docker: return 11
+    case .UNRECOGNIZED(let i): return i
+    }
+  }
+
+  // The compiler won't synthesize support with the UNRECOGNIZED case.
+  static let allCases: [Url_Protocol] = [
+    .local,
+    .ssh,
+    .docker,
+  ]
+
+}
+
+/// URL represents a pointer to a resource. It should be considered immutable.
+struct Url_URL: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Kind indicates the URL kind.
+  /// NOTE: This field number is out of order for historical reasons.
+  var kind: Url_Kind = .synchronization
+
+  /// Protocol indicates a location type.
+  var `protocol`: Url_Protocol = .local
+
+  /// User is the user under which a resource should be accessed.
+  var user: String = String()
+
+  /// Host is protocol-specific, but generally indicates the location of the
+  /// remote.
+  var host: String = String()
+
+  /// Port indicates a TCP port via which to access the remote location, if
+  /// applicable.
+  var port: UInt32 = 0
+
+  /// Path indicates the path of a resource.
+  var path: String = String()
+
+  /// Environment contains captured environment variable information. It is not
+  /// a required component and its contents and their behavior depend on the
+  /// transport implementation.
+  var environment: Dictionary<String,String> = [:]
+
+  /// Parameters are internal transport parameters. These are set for URLs
+  /// generated internally that require additional metadata. Parameters are not
+  /// required and their behavior is dependent on the transport implementation.
+  var parameters: Dictionary<String,String> = [:]
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "url"
+
+extension Url_Kind: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "Synchronization"),
+    1: .same(proto: "Forwarding"),
+  ]
+}
+
+extension Url_Protocol: SwiftProtobuf._ProtoNameProviding {
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "Local"),
+    1: .same(proto: "SSH"),
+    11: .same(proto: "Docker"),
+  ]
+}
+
+extension Url_URL: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".URL"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    7: .same(proto: "kind"),
+    1: .same(proto: "protocol"),
+    2: .same(proto: "user"),
+    3: .same(proto: "host"),
+    4: .same(proto: "port"),
+    5: .same(proto: "path"),
+    6: .same(proto: "environment"),
+    8: .same(proto: "parameters"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularEnumField(value: &self.`protocol`) }()
+      case 2: try { try decoder.decodeSingularStringField(value: &self.user) }()
+      case 3: try { try decoder.decodeSingularStringField(value: &self.host) }()
+      case 4: try { try decoder.decodeSingularUInt32Field(value: &self.port) }()
+      case 5: try { try decoder.decodeSingularStringField(value: &self.path) }()
+      case 6: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap<SwiftProtobuf.ProtobufString,SwiftProtobuf.ProtobufString>.self, value: &self.environment) }()
+      case 7: try { try decoder.decodeSingularEnumField(value: &self.kind) }()
+      case 8: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap<SwiftProtobuf.ProtobufString,SwiftProtobuf.ProtobufString>.self, value: &self.parameters) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if self.`protocol` != .local {
+      try visitor.visitSingularEnumField(value: self.`protocol`, fieldNumber: 1)
+    }
+    if !self.user.isEmpty {
+      try visitor.visitSingularStringField(value: self.user, fieldNumber: 2)
+    }
+    if !self.host.isEmpty {
+      try visitor.visitSingularStringField(value: self.host, fieldNumber: 3)
+    }
+    if self.port != 0 {
+      try visitor.visitSingularUInt32Field(value: self.port, fieldNumber: 4)
+    }
+    if !self.path.isEmpty {
+      try visitor.visitSingularStringField(value: self.path, fieldNumber: 5)
+    }
+    if !self.environment.isEmpty {
+      try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap<SwiftProtobuf.ProtobufString,SwiftProtobuf.ProtobufString>.self, value: self.environment, fieldNumber: 6)
+    }
+    if self.kind != .synchronization {
+      try visitor.visitSingularEnumField(value: self.kind, fieldNumber: 7)
+    }
+    if !self.parameters.isEmpty {
+      try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap<SwiftProtobuf.ProtobufString,SwiftProtobuf.ProtobufString>.self, value: self.parameters, fieldNumber: 8)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Url_URL, rhs: Url_URL) -> Bool {
+    if lhs.kind != rhs.kind {return false}
+    if lhs.`protocol` != rhs.`protocol` {return false}
+    if lhs.user != rhs.user {return false}
+    if lhs.host != rhs.host {return false}
+    if lhs.port != rhs.port {return false}
+    if lhs.path != rhs.path {return false}
+    if lhs.environment != rhs.environment {return false}
+    if lhs.parameters != rhs.parameters {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/url_url.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/url_url.proto
new file mode 100644
index 00000000..27cc4c00
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/url_url.proto
@@ -0,0 +1,90 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/url/url.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package url;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/url";
+
+// Kind indicates the kind of a URL.
+enum Kind {
+    // Synchronization indicates a synchronization URL.
+    Synchronization = 0;
+    // Forwarding indicates a forwarding URL.
+    Forwarding = 1;
+}
+
+// Protocol indicates a location type.
+enum Protocol {
+    // Local indicates that the resource is on the local system.
+    Local = 0;
+    // SSH indicates that the resource is accessible via SSH.
+    SSH = 1;
+
+    // Enumeration value 2 is reserved for custom protocols.
+
+    // Enumeration value 3 was previously used for the mutagen.io-based tunnel
+    // protocol. This protocol was experimental and only available as part of
+    // the v0.11.x release series. It should not be re-used.
+
+    // Enumeration values 4-10 are reserved for core protocols.
+
+    // Docker indicates that the resource is inside a Docker container.
+    Docker = 11;
+}
+
+// URL represents a pointer to a resource. It should be considered immutable.
+message URL {
+    // Kind indicates the URL kind.
+    // NOTE: This field number is out of order for historical reasons.
+    Kind kind = 7;
+    // Protocol indicates a location type.
+    Protocol protocol = 1;
+    // User is the user under which a resource should be accessed.
+    string user = 2;
+    // Host is protocol-specific, but generally indicates the location of the
+    // remote.
+    string host = 3;
+    // Port indicates a TCP port via which to access the remote location, if
+    // applicable.
+    uint32 port = 4;
+    // Path indicates the path of a resource.
+    string path = 5;
+    // Environment contains captured environment variable information. It is not
+    // a required component and its contents and their behavior depend on the
+    // transport implementation.
+    map<string, string> environment = 6;
+
+    // Field 7 is already used above for the kind field. It is out of order for
+    // historical reasons.
+
+    // Parameters are internal transport parameters. These are set for URLs
+    // generated internally that require additional metadata. Parameters are not
+    // required and their behavior is dependent on the transport implementation.
+    map<string, string> parameters = 8;
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/daemon.pb.swift b/Coder-Desktop/VPNLib/FileSync/daemon.pb.swift
deleted file mode 100644
index 047ca500..00000000
--- a/Coder-Desktop/VPNLib/FileSync/daemon.pb.swift
+++ /dev/null
@@ -1,83 +0,0 @@
-// DO NOT EDIT.
-// swift-format-ignore-file
-// swiftlint:disable all
-//
-// Generated by the Swift generator plugin for the protocol buffer compiler.
-// Source: Coder-Desktop/VPNLib/FileSync/daemon.proto
-//
-// For information on using the generated types, please see the documentation:
-//   https://github.com/apple/swift-protobuf/
-
-import SwiftProtobuf
-
-// If the compiler emits an error on this type, it is because this file
-// was generated by a version of the `protoc` Swift plug-in that is
-// incompatible with the version of SwiftProtobuf to which you are linking.
-// Please ensure that you are building against the same version of the API
-// that was used to generate this file.
-fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
-  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
-  typealias Version = _2
-}
-
-struct Daemon_TerminateRequest: Sendable {
-  // SwiftProtobuf.Message conformance is added in an extension below. See the
-  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
-  // methods supported on all messages.
-
-  var unknownFields = SwiftProtobuf.UnknownStorage()
-
-  init() {}
-}
-
-struct Daemon_TerminateResponse: Sendable {
-  // SwiftProtobuf.Message conformance is added in an extension below. See the
-  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
-  // methods supported on all messages.
-
-  var unknownFields = SwiftProtobuf.UnknownStorage()
-
-  init() {}
-}
-
-// MARK: - Code below here is support for the SwiftProtobuf runtime.
-
-fileprivate let _protobuf_package = "daemon"
-
-extension Daemon_TerminateRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
-  static let protoMessageName: String = _protobuf_package + ".TerminateRequest"
-  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
-
-  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
-    // Load everything into unknown fields
-    while try decoder.nextFieldNumber() != nil {}
-  }
-
-  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
-    try unknownFields.traverse(visitor: &visitor)
-  }
-
-  static func ==(lhs: Daemon_TerminateRequest, rhs: Daemon_TerminateRequest) -> Bool {
-    if lhs.unknownFields != rhs.unknownFields {return false}
-    return true
-  }
-}
-
-extension Daemon_TerminateResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
-  static let protoMessageName: String = _protobuf_package + ".TerminateResponse"
-  static let _protobuf_nameMap = SwiftProtobuf._NameMap()
-
-  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
-    // Load everything into unknown fields
-    while try decoder.nextFieldNumber() != nil {}
-  }
-
-  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
-    try unknownFields.traverse(visitor: &visitor)
-  }
-
-  static func ==(lhs: Daemon_TerminateResponse, rhs: Daemon_TerminateResponse) -> Bool {
-    if lhs.unknownFields != rhs.unknownFields {return false}
-    return true
-  }
-}
diff --git a/Coder-Desktop/VPNLib/FileSync/daemon.proto b/Coder-Desktop/VPNLib/FileSync/daemon.proto
deleted file mode 100644
index 4431b35d..00000000
--- a/Coder-Desktop/VPNLib/FileSync/daemon.proto
+++ /dev/null
@@ -1,11 +0,0 @@
-syntax = "proto3";
-
-package daemon;
-
-message TerminateRequest{}
-
-message TerminateResponse{}
-
-service Daemon {
-    rpc Terminate(TerminateRequest) returns (TerminateResponse) {}
-}
diff --git a/Makefile b/Makefile
index 14faf6dd..ebb8e384 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,9 @@ SCHEME := Coder\ Desktop
 TEST_PLAN := Coder-Desktop
 SWIFT_VERSION := 6.0
 
+MUTAGEN_PROTO_DEFS := $(shell find $(PROJECT)/VPNLib/FileSync/MutagenSDK -type f -name '*.proto' -print)
+MUTAGEN_PROTO_SWIFTS := $(patsubst %.proto,%.pb.swift,$(MUTAGEN_PROTO_DEFS))
+
 MUTAGEN_RESOURCES := mutagen-agents.tar.gz mutagen-darwin-arm64 mutagen-darwin-amd64
 ifndef MUTAGEN_VERSION
 MUTAGEN_VERSION:=$(shell grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' $(PROJECT)/Resources/.mutagenversion)
@@ -52,7 +55,7 @@ setup: \
 	$(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)) \
 	$(XCPROJECT) \
 	$(PROJECT)/VPNLib/vpn.pb.swift \
-	$(PROJECT)/VPNLib/FileSync/daemon.pb.swift
+	$(MUTAGEN_PROTO_SWIFTS)
 
 # Mutagen resources
 $(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)): $(PROJECT)/Resources/.mutagenversion
@@ -72,11 +75,12 @@ $(XCPROJECT): $(PROJECT)/project.yml
 $(PROJECT)/VPNLib/vpn.pb.swift: $(PROJECT)/VPNLib/vpn.proto
 	protoc --swift_opt=Visibility=public --swift_out=. 'Coder-Desktop/VPNLib/vpn.proto'
 
-$(PROJECT)/VPNLib/FileSync/daemon.pb.swift: $(PROJECT)/VPNLib/FileSync/daemon.proto
+$(MUTAGEN_PROTO_SWIFTS):
 	protoc \
-		--swift_out=.\
-		--grpc-swift_out=. \
-		'Coder-Desktop/VPNLib/FileSync/daemon.proto'
+	-I=$(PROJECT)/VPNLib/FileSync/MutagenSDK \
+	--swift_out=$(PROJECT)/VPNLib/FileSync/MutagenSDK \
+	--grpc-swift_out=$(PROJECT)/VPNLib/FileSync/MutagenSDK \
+	$(patsubst %.pb.swift,%.proto,$@)
 
 $(KEYCHAIN_FILE):
 	security create-keychain -p "" "$(APP_SIGNING_KEYCHAIN)"
@@ -164,7 +168,7 @@ clean/mutagen:
 	find $(PROJECT)/Resources -name 'mutagen-*' -delete
 
 .PHONY: proto
-proto: $(PROJECT)/VPNLib/vpn.pb.swift $(PROJECT)/VPNLib/FileSync/daemon.pb.swift ## Generate Swift files from protobufs
+proto: $(PROJECT)/VPNLib/vpn.pb.swift $(MUTAGEN_PROTO_SWIFTS) ## Generate Swift files from protobufs
 
 .PHONY: help
 help: ## Show this help
diff --git a/scripts/mutagen-proto.sh b/scripts/mutagen-proto.sh
new file mode 100755
index 00000000..4fc6cf67
--- /dev/null
+++ b/scripts/mutagen-proto.sh
@@ -0,0 +1,142 @@
+#!/usr/bin/env bash
+
+# This script vendors the Mutagen proto files from a tag on a Mutagen GitHub repo.
+# It is very similar to `Update-Proto.ps1` on `coder/coder-desktop-windows`.
+# It's very unlikely that we'll use this script regularly.
+#
+# Unlike the Go compiler, the Swift compiler does not support multiple files 
+# with the same name in different directories. 
+# To handle this, this script flattens the directory structure of the proto 
+# files into the filename, i.e. `service/synchronization/synchronization.proto`
+# becomes `service_synchronization_synchronization.proto`.
+# It also updates the proto imports to use these paths.
+
+set -euo pipefail
+
+if [ $# -ne 1 ]; then
+    echo "Usage: $0 <mutagen-tag>"
+    exit 1
+fi
+
+mutagen_tag="$1"
+
+# TODO: Change this to `coder/mutagen` once we add a version tag there
+repo="mutagen-io/mutagen"
+proto_prefix="pkg"
+# Right now, we only care about the synchronization and daemon management gRPC
+entry_files=("service/synchronization/synchronization.proto" "service/daemon/daemon.proto")
+
+out_folder="Coder-Desktop/VPNLib/FileSync/MutagenSDK"
+
+clone_dir="/tmp/coder-desktop-mutagen-proto"
+if [ -d "$clone_dir" ]; then
+    echo "Found existing mutagen repo at $clone_dir, checking out $mutagen_tag..."
+    pushd "$clone_dir" > /dev/null
+    git clean -fdx
+    
+    current_tag=$(git name-rev --name-only HEAD)
+    if [ "$current_tag" != "tags/$mutagen_tag" ]; then
+        git fetch --all
+        git checkout "$mutagen_tag"
+    fi
+    popd > /dev/null
+else
+    mkdir -p "$clone_dir"
+    echo "Cloning mutagen repo to $clone_dir..."
+    git clone --depth 1 --branch "$mutagen_tag" "https://github.com/$repo.git" "$clone_dir"
+fi
+
+# Extract MIT License header
+mit_start_line=$(grep -n "^MIT License" "$clone_dir/LICENSE" | cut -d ":" -f 1)
+if [ -z "$mit_start_line" ]; then
+    echo "Error: Failed to find MIT License header in Mutagen LICENSE file"
+    exit 1
+fi
+license_header=$(sed -n "${mit_start_line},\$p" "$clone_dir/LICENSE" | sed 's/^/ * /')
+
+declare -A file_map=()
+file_paths=()
+
+add_file() {
+    local filepath="$1"
+    local proto_path="${filepath#"$clone_dir"/"$proto_prefix"/}"
+    local flat_name
+    flat_name=$(echo "$proto_path" | sed 's/\//_/g')
+    
+    # Skip if already processed
+    if [[ -n "${file_map[$proto_path]:-}" ]]; then
+        return
+    fi
+    
+    echo "Adding $proto_path -> $flat_name"
+    file_map[$proto_path]=$flat_name
+    file_paths+=("$filepath")
+    
+    # Process imports
+    while IFS= read -r line; do
+        if [[ $line =~ ^import\ \"(.+)\" ]]; then
+            import_path="${BASH_REMATCH[1]}"
+            
+            # Ignore google imports, as they're not vendored
+            if [[ $import_path =~ ^google/ ]]; then
+                echo "Skipping $import_path"
+                continue
+            fi
+            
+            import_file_path="$clone_dir/$proto_prefix/$import_path"
+            if [ -f "$import_file_path" ]; then
+                add_file "$import_file_path"
+            else
+                echo "Error: Import $import_path not found"
+                exit 1
+            fi
+        fi
+    done < "$filepath"
+}
+
+for entry_file in "${entry_files[@]}"; do
+    entry_file_path="$clone_dir/$proto_prefix/$entry_file"
+    if [ ! -f "$entry_file_path" ]; then
+        echo "Error: Failed to find $entry_file_path in mutagen repo"
+        exit 1
+    fi
+    add_file "$entry_file_path"
+done
+
+mkdir -p "$out_folder"
+
+for file_path in "${file_paths[@]}"; do
+    proto_path="${file_path#"$clone_dir"/"$proto_prefix"/}"
+    flat_name="${file_map[$proto_path]}"
+    dst_path="$out_folder/$flat_name"
+    
+    cp -f "$file_path" "$dst_path"
+    
+    file_header="/*\n * This file was taken from\n * https://github.com/$repo/tree/$mutagen_tag/$proto_prefix/$proto_path\n *\n$license_header\n */\n\n"
+    content=$(cat "$dst_path")
+    echo -e "$file_header$content" > "$dst_path"
+    
+    tmp_file=$(mktemp)
+    while IFS= read -r line; do
+        if [[ $line =~ ^import\ \"(.+)\" ]]; then
+            import_path="${BASH_REMATCH[1]}"
+            
+            # Retain google imports
+            if [[ $import_path =~ ^google/ ]]; then
+                echo "$line" >> "$tmp_file"
+                continue
+            fi
+            
+            # Convert import path to flattened format
+            flat_import=$(echo "$import_path" | sed 's/\//_/g')
+            echo "import \"$flat_import\";" >> "$tmp_file"
+        else
+            echo "$line" >> "$tmp_file"
+        fi
+    done < "$dst_path"
+    mv "$tmp_file" "$dst_path"
+    
+    echo "Processed $proto_path -> $flat_name"
+done
+
+echo "Successfully downloaded proto files from $mutagen_tag to $out_folder"
\ No newline at end of file

From 111b30c28b178df6c6b247e65a36d9dfce5de58a Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 19 Mar 2025 18:49:02 +1100
Subject: [PATCH 10/25] fix: start coder connect on launch after SE is
 installed (#113)

This is a fix for #108. Previously we would start the VPN immediately on launch if the config option was true. This starting of the VPN raced with any concurrent upgrades of the network extension, causing the VPN to be started with a VPN config belonging to the older network extension, and producing a consistent error message:
![image](https://github.com/user-attachments/assets/a69932cb-4c86-4d45-8ab5-5843e255f395)

Instead, we should only start the VPN once we know that the system extension and VPN configuration are installed.
---
 .../Coder-Desktop/Coder_DesktopApp.swift      | 11 +++++++---
 .../Coder-Desktop/VPN/VPNService.swift        | 22 ++++++++++++++-----
 .../Coder-DesktopTests/LoginFormTests.swift   |  6 +++++
 3 files changed, 31 insertions(+), 8 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index a8d0c946..091a1c25 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -37,6 +37,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
         vpn = CoderVPNService()
         state = AppState(onChange: vpn.configureTunnelProviderProtocol)
         fileSyncDaemon = MutagenDaemon()
+        if state.startVPNOnLaunch {
+            vpn.startWhenReady = true
+        }
+        vpn.installSystemExtension()
     }
 
     func applicationDidFinishLaunching(_: Notification) {
@@ -68,9 +72,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
             if await !vpn.loadNetworkExtensionConfig() {
                 state.reconfigure()
             }
-            if state.startVPNOnLaunch {
-                await vpn.start()
-            }
         }
         // TODO: Start the daemon only once a file sync is configured
         Task {
@@ -78,6 +79,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
         }
     }
 
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+    }
+
     // This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
     // or return `.terminateNow`
     func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
index ca0a8ff3..22a3ad8b 100644
--- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
+++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
@@ -18,6 +18,16 @@ enum VPNServiceState: Equatable {
     case disconnecting
     case connected
     case failed(VPNServiceError)
+
+    var canBeStarted: Bool {
+        switch self {
+        // A tunnel failure should not prevent a reconnect attempt
+        case .disabled, .failed:
+            true
+        default:
+            false
+        }
+    }
 }
 
 enum VPNServiceError: Error, Equatable {
@@ -54,11 +64,18 @@ final class CoderVPNService: NSObject, VPNService {
         guard neState == .enabled || neState == .disabled else {
             return .failed(.networkExtensionError(neState))
         }
+        if startWhenReady, tunnelState.canBeStarted {
+            startWhenReady = false
+            Task { await start() }
+        }
         return tunnelState
     }
 
     @Published var menuState: VPNMenuState = .init()
 
+    // Whether the VPN should start as soon as possible
+    var startWhenReady: Bool = false
+
     // systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get
     // garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework
     // only stores a weak reference to the delegate.
@@ -68,11 +85,6 @@ final class CoderVPNService: NSObject, VPNService {
 
     override init() {
         super.init()
-        installSystemExtension()
-    }
-
-    deinit {
-        NotificationCenter.default.removeObserver(self)
     }
 
     func start() async {
diff --git a/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift b/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift
index a07ced3f..26f5883d 100644
--- a/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift
@@ -107,6 +107,12 @@ struct LoginTests {
             data: [.get: Client.encoder.encode(buildInfo)]
         ).register()
 
+        try Mock(
+            url: url.appendingPathComponent("/api/v2/users/me"),
+            statusCode: 200,
+            data: [.get: Client.encoder.encode(User(id: UUID(), username: "username"))]
+        ).register()
+
         try await ViewHosting.host(view) {
             try await sut.inspection.inspect { view in
                 try view.find(ViewType.TextField.self).setInput(url.absoluteString)

From 2603ace63cbdb3949ce0cc43543be1da971568b6 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 19 Mar 2025 18:52:31 +1100
Subject: [PATCH 11/25] chore: prompt for sign in when turning VPN on if signed
 out (#114)

Closes #106.
---
 .../Preview Content/PreviewVPN.swift            |  2 ++
 .../Coder-Desktop/VPN/VPNService.swift          |  1 +
 Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift | 17 ++++++++++++++---
 Coder-Desktop/Coder-DesktopTests/Util.swift     |  1 +
 .../Coder-DesktopTests/VPNMenuTests.swift       |  2 +-
 5 files changed, 19 insertions(+), 4 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift
index 4faa10fb..a3ef51e5 100644
--- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift	
+++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift	
@@ -78,4 +78,6 @@ final class PreviewVPN: Coder_Desktop.VPNService {
     func configureTunnelProviderProtocol(proto _: NETunnelProviderProtocol?) {
         state = .connecting
     }
+
+    var startWhenReady: Bool = false
 }
diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
index 22a3ad8b..50078d5f 100644
--- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
+++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
@@ -10,6 +10,7 @@ protocol VPNService: ObservableObject {
     func start() async
     func stop() async
     func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?)
+    var startWhenReady: Bool { get set }
 }
 
 enum VPNServiceState: Equatable {
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift b/Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift
index 352123de..c3c44dba 100644
--- a/Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift
@@ -4,6 +4,7 @@ struct VPNMenu<VPN: VPNService>: View {
     @EnvironmentObject var vpn: VPN
     @EnvironmentObject var state: AppState
     @Environment(\.openSettings) private var openSettings
+    @Environment(\.openWindow) private var openWindow
 
     let inspection = Inspection<Self>()
 
@@ -16,7 +17,18 @@ struct VPNMenu<VPN: VPNService>: View {
                     Toggle(isOn: Binding(
                         get: { vpn.state == .connected || vpn.state == .connecting },
                         set: { isOn in Task {
-                            if isOn { await vpn.start() } else { await vpn.stop() }
+                            if isOn {
+                                // Clicking the toggle while logged out should
+                                // open the login window, then start the VPN asap
+                                if !state.hasSession {
+                                    vpn.startWhenReady = true
+                                    openWindow(id: .login)
+                                } else {
+                                    await vpn.start()
+                                }
+                            } else {
+                                await vpn.stop()
+                            }
                         }
                         }
                     )) {
@@ -86,8 +98,7 @@ struct VPNMenu<VPN: VPNService>: View {
     }
 
     private var vpnDisabled: Bool {
-        !state.hasSession ||
-            vpn.state == .connecting ||
+        vpn.state == .connecting ||
             vpn.state == .disconnecting ||
             // Prevent starting the VPN before the user has approved the system extension.
             vpn.state == .failed(.systemExtensionError(.needsUserApproval))
diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
index 4b1d0e7c..c41f5c19 100644
--- a/Coder-Desktop/Coder-DesktopTests/Util.swift
+++ b/Coder-Desktop/Coder-DesktopTests/Util.swift
@@ -23,6 +23,7 @@ class MockVPNService: VPNService, ObservableObject {
     }
 
     func configureTunnelProviderProtocol(proto _: NETunnelProviderProtocol?) {}
+    var startWhenReady: Bool = false
 }
 
 extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}
diff --git a/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift
index c38a062d..616e3c53 100644
--- a/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift
@@ -23,7 +23,7 @@ struct VPNMenuTests {
         try await ViewHosting.host(view) {
             try await sut.inspection.inspect { view in
                 let toggle = try view.find(ViewType.Toggle.self)
-                #expect(toggle.isDisabled())
+                #expect(!toggle.isDisabled())
                 #expect(throws: Never.self) { try view.find(text: "Sign in to use Coder Desktop") }
                 #expect(throws: Never.self) { try view.find(button: "Sign in") }
             }

From 4fb797074a491216430bcee01b7499159defc4ce Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Mon, 24 Mar 2025 20:49:24 +1100
Subject: [PATCH 12/25] chore: conditionally start file sync daemon (#115)

This makes a few improvements to #98:
- The mutagen path & data directory can be now be configured on the MutagenDaemon, to support overriding it in tests (coming soon).
- A mutagen daemon failure now kills the process, such that can it be restarted (TBC).
- Makes start & stop transitions mutually exclusive via a semaphore, to account for actor re-entrancy.
- The start operation now waits for the daemon to respond to a version request before completing.
- The daemon is always started on launch, but then immediately stopped if it doesn't manage any file sync sessions, as to not run in the background unncessarily.
---
 .../Coder-Desktop/Coder_DesktopApp.swift      |  13 +-
 .../VPNLib/FileSync/FileSyncDaemon.swift      | 190 +++++++++++++++---
 Coder-Desktop/project.yml                     |   6 +-
 3 files changed, 178 insertions(+), 31 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index 091a1c25..f2c7b20a 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -36,11 +36,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
     override init() {
         vpn = CoderVPNService()
         state = AppState(onChange: vpn.configureTunnelProviderProtocol)
-        fileSyncDaemon = MutagenDaemon()
         if state.startVPNOnLaunch {
             vpn.startWhenReady = true
         }
         vpn.installSystemExtension()
+        #if arch(arm64)
+        let mutagenBinary = "mutagen-darwin-arm64"
+        #elseif arch(x86_64)
+        let mutagenBinary = "mutagen-darwin-amd64"
+        #endif
+        fileSyncDaemon = MutagenDaemon(
+            mutagenPath: Bundle.main.url(forResource: mutagenBinary, withExtension: nil)
+        )
     }
 
     func applicationDidFinishLaunching(_: Notification) {
@@ -73,10 +80,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
                 state.reconfigure()
             }
         }
-        // TODO: Start the daemon only once a file sync is configured
-        Task {
-            await fileSyncDaemon.start()
-        }
     }
 
     deinit {
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
index 9324c076..68446940 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
@@ -2,13 +2,26 @@ import Foundation
 import GRPC
 import NIO
 import os
+import Semaphore
 import Subprocess
+import SwiftUI
 
 @MainActor
 public protocol FileSyncDaemon: ObservableObject {
     var state: DaemonState { get }
-    func start() async
+    func start() async throws(DaemonError)
     func stop() async
+    func listSessions() async throws -> [FileSyncSession]
+    func createSession(with: FileSyncSession) async throws
+}
+
+public struct FileSyncSession {
+    public let id: String
+    public let name: String
+    public let localPath: URL
+    public let workspace: String
+    public let agent: String
+    public let remotePath: URL
 }
 
 @MainActor
@@ -17,7 +30,14 @@ public class MutagenDaemon: FileSyncDaemon {
 
     @Published public var state: DaemonState = .stopped {
         didSet {
-            logger.info("daemon state changed: \(self.state.description, privacy: .public)")
+            logger.info("daemon state set: \(self.state.description, privacy: .public)")
+            if case .failed = state {
+                Task {
+                    try? await cleanupGRPC()
+                }
+                mutagenProcess?.kill()
+                mutagenProcess = nil
+            }
         }
     }
 
@@ -26,46 +46,61 @@ public class MutagenDaemon: FileSyncDaemon {
     private let mutagenDataDirectory: URL
     private let mutagenDaemonSocket: URL
 
+    // Non-nil when the daemon is running
     private var group: MultiThreadedEventLoopGroup?
     private var channel: GRPCChannel?
-    private var client: Daemon_DaemonAsyncClient?
-
-    public init() {
-        #if arch(arm64)
-            mutagenPath = Bundle.main.url(forResource: "mutagen-darwin-arm64", withExtension: nil)
-        #elseif arch(x86_64)
-            mutagenPath = Bundle.main.url(forResource: "mutagen-darwin-amd64", withExtension: nil)
-        #else
-            fatalError("unknown architecture")
-        #endif
-        mutagenDataDirectory = FileManager.default.urls(
-            for: .applicationSupportDirectory,
-            in: .userDomainMask
-        ).first!.appending(path: "Coder Desktop").appending(path: "Mutagen")
+    private var client: DaemonClient?
+
+    // Protect start & stop transitions against re-entrancy
+    private let transition = AsyncSemaphore(value: 1)
+
+    public init(mutagenPath: URL? = nil,
+                mutagenDataDirectory: URL = FileManager.default.urls(
+                    for: .applicationSupportDirectory,
+                    in: .userDomainMask
+                ).first!.appending(path: "Coder Desktop").appending(path: "Mutagen"))
+    {
+        self.mutagenPath = mutagenPath
+        self.mutagenDataDirectory = mutagenDataDirectory
         mutagenDaemonSocket = mutagenDataDirectory.appending(path: "daemon").appending(path: "daemon.sock")
         // It shouldn't be fatal if the app was built without Mutagen embedded,
         // but file sync will be unavailable.
         if mutagenPath == nil {
             logger.warning("Mutagen not embedded in app, file sync will be unavailable")
             state = .unavailable
+            return
+        }
+
+        // If there are sync sessions, the daemon should be running
+        Task {
+            do throws(DaemonError) {
+                try await start()
+            } catch {
+                state = .failed(error)
+                return
+            }
+            await stopIfNoSessions()
         }
     }
 
-    public func start() async {
+    public func start() async throws(DaemonError) {
         if case .unavailable = state { return }
 
         // Stop an orphaned daemon, if there is one
         try? await connect()
         await stop()
 
+        await transition.wait()
+        defer { transition.signal() }
+        logger.info("starting mutagen daemon")
+
         mutagenProcess = createMutagenProcess()
         // swiftlint:disable:next large_tuple
         let (standardOutput, standardError, waitForExit): (Pipe.AsyncBytes, Pipe.AsyncBytes, @Sendable () async -> Void)
         do {
             (standardOutput, standardError, waitForExit) = try mutagenProcess!.run()
         } catch {
-            state = .failed(DaemonError.daemonStartFailure(error))
-            return
+            throw .daemonStartFailure(error)
         }
 
         Task {
@@ -85,10 +120,11 @@ public class MutagenDaemon: FileSyncDaemon {
         do {
             try await connect()
         } catch {
-            state = .failed(DaemonError.daemonStartFailure(error))
-            return
+            throw .daemonStartFailure(error)
         }
 
+        try await waitForDaemonStart()
+
         state = .running
         logger.info(
             """
@@ -98,6 +134,34 @@ public class MutagenDaemon: FileSyncDaemon {
         )
     }
 
+    // The daemon takes a moment to open the socket, and we don't want to hog the main actor
+    // so poll for it on a background thread
+    private func waitForDaemonStart(
+        maxAttempts: Int = 5,
+        attemptInterval: Duration = .milliseconds(100)
+    ) async throws(DaemonError) {
+        do {
+            try await Task.detached(priority: .background) {
+                for attempt in 0 ... maxAttempts {
+                    do {
+                        _ = try await self.client!.mgmt.version(
+                            Daemon_VersionRequest(),
+                            callOptions: .init(timeLimit: .timeout(.milliseconds(500)))
+                        )
+                        return
+                    } catch {
+                        if attempt == maxAttempts {
+                            throw error
+                        }
+                        try? await Task.sleep(for: attemptInterval)
+                    }
+                }
+            }.value
+        } catch {
+            throw .daemonStartFailure(error)
+        }
+    }
+
     private func connect() async throws(DaemonError) {
         guard client == nil else {
             // Already connected
@@ -110,14 +174,17 @@ public class MutagenDaemon: FileSyncDaemon {
                 transportSecurity: .plaintext,
                 eventLoopGroup: group!
             )
-            client = Daemon_DaemonAsyncClient(channel: channel!)
+            client = DaemonClient(
+                mgmt: Daemon_DaemonAsyncClient(channel: channel!),
+                sync: Synchronization_SynchronizationAsyncClient(channel: channel!)
+            )
             logger.info(
                 "Successfully connected to mutagen daemon, socket: \(self.mutagenDaemonSocket.path, privacy: .public)"
             )
         } catch {
             logger.error("Failed to connect to gRPC: \(error)")
             try? await cleanupGRPC()
-            throw DaemonError.connectionFailure(error)
+            throw .connectionFailure(error)
         }
     }
 
@@ -132,6 +199,10 @@ public class MutagenDaemon: FileSyncDaemon {
 
     public func stop() async {
         if case .unavailable = state { return }
+        await transition.wait()
+        defer { transition.signal() }
+        logger.info("stopping mutagen daemon")
+
         state = .stopped
         guard FileManager.default.fileExists(atPath: mutagenDaemonSocket.path) else {
             // Already stopped
@@ -140,7 +211,7 @@ public class MutagenDaemon: FileSyncDaemon {
 
         // "We don't check the response or error, because the daemon
         // may terminate before it has a chance to send the response."
-        _ = try? await client?.terminate(
+        _ = try? await client?.mgmt.terminate(
             Daemon_TerminateRequest(),
             callOptions: .init(timeLimit: .timeout(.milliseconds(500)))
         )
@@ -175,6 +246,7 @@ public class MutagenDaemon: FileSyncDaemon {
                 """
             )
             state = .failed(.terminatedUnexpectedly)
+            return
         }
     }
 
@@ -183,6 +255,55 @@ public class MutagenDaemon: FileSyncDaemon {
             logger.info("\(line, privacy: .public)")
         }
     }
+
+    public func listSessions() async throws -> [FileSyncSession] {
+        guard case .running = state else {
+            return []
+        }
+        // TODO: Implement
+        return []
+    }
+
+    public func createSession(with _: FileSyncSession) async throws {
+        if case .stopped = state {
+            do throws(DaemonError) {
+                try await start()
+            } catch {
+                state = .failed(error)
+                return
+            }
+        }
+        // TODO: Add Session
+    }
+
+    public func deleteSession() async throws {
+        // TODO: Delete session
+        await stopIfNoSessions()
+    }
+
+    private func stopIfNoSessions() async {
+        let sessions: Synchronization_ListResponse
+        do {
+            sessions = try await client!.sync.list(Synchronization_ListRequest.with { req in
+                req.selection = .with { selection in
+                    selection.all = true
+                }
+            })
+        } catch {
+            state = .failed(.daemonStartFailure(error))
+            return
+        }
+        // If there's no configured sessions, the daemon doesn't need to be running
+        if sessions.sessionStates.isEmpty {
+            logger.info("No sync sessions found")
+            await stop()
+        }
+    }
+}
+
+struct DaemonClient {
+    let mgmt: Daemon_DaemonAsyncClient
+    let sync: Synchronization_SynchronizationAsyncClient
 }
 
 public enum DaemonState {
@@ -191,7 +312,7 @@ public enum DaemonState {
     case failed(DaemonError)
     case unavailable
 
-    var description: String {
+    public var description: String {
         switch self {
         case .running:
             "Running"
@@ -203,12 +324,27 @@ public enum DaemonState {
             "Unavailable"
         }
     }
+
+    public var color: Color {
+        switch self {
+        case .running:
+            .green
+        case .stopped:
+            .gray
+        case .failed:
+            .red
+        case .unavailable:
+            .gray
+        }
+    }
 }
 
 public enum DaemonError: Error {
+    case daemonNotRunning
     case daemonStartFailure(Error)
     case connectionFailure(Error)
     case terminatedUnexpectedly
+    case grpcFailure(Error)
 
     var description: String {
         switch self {
@@ -218,6 +354,10 @@ public enum DaemonError: Error {
             "Connection failure: \(error)"
         case .terminatedUnexpectedly:
             "Daemon terminated unexpectedly"
+        case .daemonNotRunning:
+            "The daemon must be started first"
+        case let .grpcFailure(error):
+            "Failed to communicate with daemon: \(error)"
         }
     }
 
diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml
index c3c53f99..fb38d35a 100644
--- a/Coder-Desktop/project.yml
+++ b/Coder-Desktop/project.yml
@@ -116,7 +116,10 @@ packages:
     exactVersion: 1.24.2
   Subprocess:
     url: https://github.com/jamf/Subprocess
-    revision: 9d67b79 
+    revision: 9d67b79
+  Semaphore:
+    url: https://github.com/groue/Semaphore/
+    exactVersion: 0.1.0
 
 targets:
   Coder Desktop:
@@ -276,6 +279,7 @@ targets:
         product: SwiftProtobufPluginLibrary
       - package: GRPC
       - package: Subprocess
+      - package: Semaphore
       - target: CoderSDK
         embed: false
 

From f53a99fbd5e6617445decad0e3dd866a6455c661 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 25 Mar 2025 00:26:40 +1100
Subject: [PATCH 13/25] ci: bump actions/upload-artifact from 4.6.1 to 4.6.2 in
 the github-actions group (#120)

Signed-off-by: dependabot[bot] <support@github.com>
---
 .github/workflows/release.yml                      | 2 +-
 Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c86eb175..c5129913 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -61,7 +61,7 @@ jobs:
       # Upload as artifact in dry-run mode
       - name: Upload Build Artifact
         if: ${{ inputs.dryrun }}
-        uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
         with:
           name: coder-desktop-build
           path: ${{ github.workspace }}/outputs/out
diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index f2c7b20a..29b0910c 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -41,9 +41,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
         }
         vpn.installSystemExtension()
         #if arch(arm64)
-        let mutagenBinary = "mutagen-darwin-arm64"
+            let mutagenBinary = "mutagen-darwin-arm64"
         #elseif arch(x86_64)
-        let mutagenBinary = "mutagen-darwin-amd64"
+            let mutagenBinary = "mutagen-darwin-amd64"
         #endif
         fileSyncDaemon = MutagenDaemon(
             mutagenPath: Bundle.main.url(forResource: mutagenBinary, withExtension: nil)

From d311dda9339f4e54aa597623e7cdd02517f700ba Mon Sep 17 00:00:00 2001
From: Spike Curtis <spike@coder.com>
Date: Thu, 27 Mar 2025 10:54:53 +0400
Subject: [PATCH 14/25] feat: add enrichment of StartRequest with OS, device
 ID, version (#123)

Enriches `StartRequest` protocol message with device ID, OS, and version, for Coder Desktop telemetry.
---
 Coder-Desktop/VPN/Manager.swift               |   3 +
 Coder-Desktop/VPNLib/Speaker.swift            |   7 +-
 Coder-Desktop/VPNLib/TelemetryEnricher.swift  |  31 ++
 Coder-Desktop/VPNLib/vpn.pb.swift             | 492 ++++++++++++++++++
 Coder-Desktop/VPNLib/vpn.proto                |  49 ++
 Coder-Desktop/VPNLibTests/SpeakerTests.swift  |   5 +-
 .../VPNLibTests/TelemetryEnricherTests.swift  |  25 +
 7 files changed, 608 insertions(+), 4 deletions(-)
 create mode 100644 Coder-Desktop/VPNLib/TelemetryEnricher.swift
 create mode 100644 Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift

diff --git a/Coder-Desktop/VPN/Manager.swift b/Coder-Desktop/VPN/Manager.swift
index a1dc6bc0..adff1434 100644
--- a/Coder-Desktop/VPN/Manager.swift
+++ b/Coder-Desktop/VPN/Manager.swift
@@ -6,6 +6,7 @@ import VPNLib
 actor Manager {
     let ptp: PacketTunnelProvider
     let cfg: ManagerConfig
+    let telemetryEnricher: TelemetryEnricher
 
     let tunnelHandle: TunnelHandle
     let speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>
@@ -19,6 +20,7 @@ actor Manager {
     init(with: PacketTunnelProvider, cfg: ManagerConfig) async throws(ManagerError) {
         ptp = with
         self.cfg = cfg
+        telemetryEnricher = TelemetryEnricher()
         #if arch(arm64)
             let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-darwin-arm64.dylib")
         #elseif arch(x86_64)
@@ -176,6 +178,7 @@ actor Manager {
                                 req.value = header.value
                             }
                         }
+                        req = telemetryEnricher.enrich(req)
                     }
                 })
         } catch {
diff --git a/Coder-Desktop/VPNLib/Speaker.swift b/Coder-Desktop/VPNLib/Speaker.swift
index b53f50a8..88e46b05 100644
--- a/Coder-Desktop/VPNLib/Speaker.swift
+++ b/Coder-Desktop/VPNLib/Speaker.swift
@@ -88,8 +88,11 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag
 
     /// Does the VPN Protocol handshake and validates the result
     public func handshake() async throws(HandshakeError) {
-        let hndsh = Handshaker(writeFD: writeFD, dispatch: dispatch, queue: queue, role: role)
-        // ignore the version for now because we know it can only be 1.0
+        let hndsh = Handshaker(writeFD: writeFD, dispatch: dispatch, queue: queue, role: role,
+                               versions: [ProtoVersion(1, 1)])
+        // ignore the version for now because we know it can only be 1.0 or 1.1.
+        // 1.1 adds support for telemetry to StartRequest, but since setting these
+        // fields won't adversely affect a 1.0 speaker, we set them regardless.
         try _ = await hndsh.handshake()
     }
 
diff --git a/Coder-Desktop/VPNLib/TelemetryEnricher.swift b/Coder-Desktop/VPNLib/TelemetryEnricher.swift
new file mode 100644
index 00000000..46ba1a17
--- /dev/null
+++ b/Coder-Desktop/VPNLib/TelemetryEnricher.swift
@@ -0,0 +1,31 @@
+import Foundation
+
+public struct TelemetryEnricher {
+    private let deviceID: String
+    private let version: String?
+
+    public init() {
+        version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
+
+        let userDefaults = UserDefaults.standard
+        let key = "deviceID"
+
+        if let existingID = userDefaults.string(forKey: key) {
+            deviceID = existingID
+        } else {
+            let newID = UUID().uuidString
+            userDefaults.set(newID, forKey: key)
+            deviceID = newID
+        }
+    }
+
+    public func enrich(_ original: Vpn_StartRequest) -> Vpn_StartRequest {
+        var req = original
+        req.deviceOs = "macOS"
+        req.deviceID = deviceID
+        if let version {
+            req.coderDesktopVersion = version
+        }
+        return req
+    }
+}
diff --git a/Coder-Desktop/VPNLib/vpn.pb.swift b/Coder-Desktop/VPNLib/vpn.pb.swift
index 525f55bb..3e728045 100644
--- a/Coder-Desktop/VPNLib/vpn.pb.swift
+++ b/Coder-Desktop/VPNLib/vpn.pb.swift
@@ -175,6 +175,118 @@ public struct Vpn_TunnelMessage: Sendable {
   fileprivate var _rpc: Vpn_RPC? = nil
 }
 
+/// ClientMessage is a message from the client (to the service). Windows only.
+public struct Vpn_ClientMessage: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  public var rpc: Vpn_RPC {
+    get {return _rpc ?? Vpn_RPC()}
+    set {_rpc = newValue}
+  }
+  /// Returns true if `rpc` has been explicitly set.
+  public var hasRpc: Bool {return self._rpc != nil}
+  /// Clears the value of `rpc`. Subsequent reads from it will return its default value.
+  public mutating func clearRpc() {self._rpc = nil}
+
+  public var msg: Vpn_ClientMessage.OneOf_Msg? = nil
+
+  public var start: Vpn_StartRequest {
+    get {
+      if case .start(let v)? = msg {return v}
+      return Vpn_StartRequest()
+    }
+    set {msg = .start(newValue)}
+  }
+
+  public var stop: Vpn_StopRequest {
+    get {
+      if case .stop(let v)? = msg {return v}
+      return Vpn_StopRequest()
+    }
+    set {msg = .stop(newValue)}
+  }
+
+  public var status: Vpn_StatusRequest {
+    get {
+      if case .status(let v)? = msg {return v}
+      return Vpn_StatusRequest()
+    }
+    set {msg = .status(newValue)}
+  }
+
+  public var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  public enum OneOf_Msg: Equatable, Sendable {
+    case start(Vpn_StartRequest)
+    case stop(Vpn_StopRequest)
+    case status(Vpn_StatusRequest)
+
+  }
+
+  public init() {}
+
+  fileprivate var _rpc: Vpn_RPC? = nil
+}
+
+/// ServiceMessage is a message from the service (to the client). Windows only.
+public struct Vpn_ServiceMessage: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  public var rpc: Vpn_RPC {
+    get {return _rpc ?? Vpn_RPC()}
+    set {_rpc = newValue}
+  }
+  /// Returns true if `rpc` has been explicitly set.
+  public var hasRpc: Bool {return self._rpc != nil}
+  /// Clears the value of `rpc`. Subsequent reads from it will return its default value.
+  public mutating func clearRpc() {self._rpc = nil}
+
+  public var msg: Vpn_ServiceMessage.OneOf_Msg? = nil
+
+  public var start: Vpn_StartResponse {
+    get {
+      if case .start(let v)? = msg {return v}
+      return Vpn_StartResponse()
+    }
+    set {msg = .start(newValue)}
+  }
+
+  public var stop: Vpn_StopResponse {
+    get {
+      if case .stop(let v)? = msg {return v}
+      return Vpn_StopResponse()
+    }
+    set {msg = .stop(newValue)}
+  }
+
+  /// either in reply to a StatusRequest or broadcasted
+  public var status: Vpn_Status {
+    get {
+      if case .status(let v)? = msg {return v}
+      return Vpn_Status()
+    }
+    set {msg = .status(newValue)}
+  }
+
+  public var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  public enum OneOf_Msg: Equatable, Sendable {
+    case start(Vpn_StartResponse)
+    case stop(Vpn_StopResponse)
+    /// either in reply to a StatusRequest or broadcasted
+    case status(Vpn_Status)
+
+  }
+
+  public init() {}
+
+  fileprivate var _rpc: Vpn_RPC? = nil
+}
+
 /// Log is a log message generated by the tunnel.  The manager should log it to the system log. It is
 /// one-way tunnel -> manager with no response.
 public struct Vpn_Log: Sendable {
@@ -599,6 +711,15 @@ public struct Vpn_StartRequest: Sendable {
 
   public var headers: [Vpn_StartRequest.Header] = []
 
+  /// Device ID from Coder Desktop
+  public var deviceID: String = String()
+
+  /// Device OS from Coder Desktop
+  public var deviceOs: String = String()
+
+  /// Coder Desktop version
+  public var coderDesktopVersion: String = String()
+
   public var unknownFields = SwiftProtobuf.UnknownStorage()
 
   /// Additional HTTP headers added to all requests
@@ -661,6 +782,94 @@ public struct Vpn_StopResponse: Sendable {
   public init() {}
 }
 
+/// StatusRequest is a request to get the status of the tunnel. The manager
+/// replies with a Status.
+public struct Vpn_StatusRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  public var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  public init() {}
+}
+
+/// Status is sent in response to a StatusRequest or broadcasted to all clients
+/// when the status changes.
+public struct Vpn_Status: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  public var lifecycle: Vpn_Status.Lifecycle = .unknown
+
+  public var errorMessage: String = String()
+
+  /// This will be a FULL update with all workspaces and agents, so clients
+  /// should replace their current peer state. Only the Upserted fields will
+  /// be populated.
+  public var peerUpdate: Vpn_PeerUpdate {
+    get {return _peerUpdate ?? Vpn_PeerUpdate()}
+    set {_peerUpdate = newValue}
+  }
+  /// Returns true if `peerUpdate` has been explicitly set.
+  public var hasPeerUpdate: Bool {return self._peerUpdate != nil}
+  /// Clears the value of `peerUpdate`. Subsequent reads from it will return its default value.
+  public mutating func clearPeerUpdate() {self._peerUpdate = nil}
+
+  public var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  public enum Lifecycle: SwiftProtobuf.Enum, Swift.CaseIterable {
+    public typealias RawValue = Int
+    case unknown // = 0
+    case starting // = 1
+    case started // = 2
+    case stopping // = 3
+    case stopped // = 4
+    case UNRECOGNIZED(Int)
+
+    public init() {
+      self = .unknown
+    }
+
+    public init?(rawValue: Int) {
+      switch rawValue {
+      case 0: self = .unknown
+      case 1: self = .starting
+      case 2: self = .started
+      case 3: self = .stopping
+      case 4: self = .stopped
+      default: self = .UNRECOGNIZED(rawValue)
+      }
+    }
+
+    public var rawValue: Int {
+      switch self {
+      case .unknown: return 0
+      case .starting: return 1
+      case .started: return 2
+      case .stopping: return 3
+      case .stopped: return 4
+      case .UNRECOGNIZED(let i): return i
+      }
+    }
+
+    // The compiler won't synthesize support with the UNRECOGNIZED case.
+    public static let allCases: [Vpn_Status.Lifecycle] = [
+      .unknown,
+      .starting,
+      .started,
+      .stopping,
+      .stopped,
+    ]
+
+  }
+
+  public init() {}
+
+  fileprivate var _peerUpdate: Vpn_PeerUpdate? = nil
+}
+
 // MARK: - Code below here is support for the SwiftProtobuf runtime.
 
 fileprivate let _protobuf_package = "vpn"
@@ -945,6 +1154,194 @@ extension Vpn_TunnelMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
   }
 }
 
+extension Vpn_ClientMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  public static let protoMessageName: String = _protobuf_package + ".ClientMessage"
+  public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "rpc"),
+    2: .same(proto: "start"),
+    3: .same(proto: "stop"),
+    4: .same(proto: "status"),
+  ]
+
+  public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularMessageField(value: &self._rpc) }()
+      case 2: try {
+        var v: Vpn_StartRequest?
+        var hadOneofValue = false
+        if let current = self.msg {
+          hadOneofValue = true
+          if case .start(let m) = current {v = m}
+        }
+        try decoder.decodeSingularMessageField(value: &v)
+        if let v = v {
+          if hadOneofValue {try decoder.handleConflictingOneOf()}
+          self.msg = .start(v)
+        }
+      }()
+      case 3: try {
+        var v: Vpn_StopRequest?
+        var hadOneofValue = false
+        if let current = self.msg {
+          hadOneofValue = true
+          if case .stop(let m) = current {v = m}
+        }
+        try decoder.decodeSingularMessageField(value: &v)
+        if let v = v {
+          if hadOneofValue {try decoder.handleConflictingOneOf()}
+          self.msg = .stop(v)
+        }
+      }()
+      case 4: try {
+        var v: Vpn_StatusRequest?
+        var hadOneofValue = false
+        if let current = self.msg {
+          hadOneofValue = true
+          if case .status(let m) = current {v = m}
+        }
+        try decoder.decodeSingularMessageField(value: &v)
+        if let v = v {
+          if hadOneofValue {try decoder.handleConflictingOneOf()}
+          self.msg = .status(v)
+        }
+      }()
+      default: break
+      }
+    }
+  }
+
+  public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    try { if let v = self._rpc {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
+    } }()
+    switch self.msg {
+    case .start?: try {
+      guard case .start(let v)? = self.msg else { preconditionFailure() }
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+    }()
+    case .stop?: try {
+      guard case .stop(let v)? = self.msg else { preconditionFailure() }
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
+    }()
+    case .status?: try {
+      guard case .status(let v)? = self.msg else { preconditionFailure() }
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
+    }()
+    case nil: break
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  public static func ==(lhs: Vpn_ClientMessage, rhs: Vpn_ClientMessage) -> Bool {
+    if lhs._rpc != rhs._rpc {return false}
+    if lhs.msg != rhs.msg {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Vpn_ServiceMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  public static let protoMessageName: String = _protobuf_package + ".ServiceMessage"
+  public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "rpc"),
+    2: .same(proto: "start"),
+    3: .same(proto: "stop"),
+    4: .same(proto: "status"),
+  ]
+
+  public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularMessageField(value: &self._rpc) }()
+      case 2: try {
+        var v: Vpn_StartResponse?
+        var hadOneofValue = false
+        if let current = self.msg {
+          hadOneofValue = true
+          if case .start(let m) = current {v = m}
+        }
+        try decoder.decodeSingularMessageField(value: &v)
+        if let v = v {
+          if hadOneofValue {try decoder.handleConflictingOneOf()}
+          self.msg = .start(v)
+        }
+      }()
+      case 3: try {
+        var v: Vpn_StopResponse?
+        var hadOneofValue = false
+        if let current = self.msg {
+          hadOneofValue = true
+          if case .stop(let m) = current {v = m}
+        }
+        try decoder.decodeSingularMessageField(value: &v)
+        if let v = v {
+          if hadOneofValue {try decoder.handleConflictingOneOf()}
+          self.msg = .stop(v)
+        }
+      }()
+      case 4: try {
+        var v: Vpn_Status?
+        var hadOneofValue = false
+        if let current = self.msg {
+          hadOneofValue = true
+          if case .status(let m) = current {v = m}
+        }
+        try decoder.decodeSingularMessageField(value: &v)
+        if let v = v {
+          if hadOneofValue {try decoder.handleConflictingOneOf()}
+          self.msg = .status(v)
+        }
+      }()
+      default: break
+      }
+    }
+  }
+
+  public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    try { if let v = self._rpc {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
+    } }()
+    switch self.msg {
+    case .start?: try {
+      guard case .start(let v)? = self.msg else { preconditionFailure() }
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
+    }()
+    case .stop?: try {
+      guard case .stop(let v)? = self.msg else { preconditionFailure() }
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
+    }()
+    case .status?: try {
+      guard case .status(let v)? = self.msg else { preconditionFailure() }
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
+    }()
+    case nil: break
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  public static func ==(lhs: Vpn_ServiceMessage, rhs: Vpn_ServiceMessage) -> Bool {
+    if lhs._rpc != rhs._rpc {return false}
+    if lhs.msg != rhs.msg {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
 extension Vpn_Log: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
   public static let protoMessageName: String = _protobuf_package + ".Log"
   public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@@ -1650,6 +2047,9 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
     2: .standard(proto: "coder_url"),
     3: .standard(proto: "api_token"),
     4: .same(proto: "headers"),
+    5: .standard(proto: "device_id"),
+    6: .standard(proto: "device_os"),
+    7: .standard(proto: "coder_desktop_version"),
   ]
 
   public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@@ -1662,6 +2062,9 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
       case 2: try { try decoder.decodeSingularStringField(value: &self.coderURL) }()
       case 3: try { try decoder.decodeSingularStringField(value: &self.apiToken) }()
       case 4: try { try decoder.decodeRepeatedMessageField(value: &self.headers) }()
+      case 5: try { try decoder.decodeSingularStringField(value: &self.deviceID) }()
+      case 6: try { try decoder.decodeSingularStringField(value: &self.deviceOs) }()
+      case 7: try { try decoder.decodeSingularStringField(value: &self.coderDesktopVersion) }()
       default: break
       }
     }
@@ -1680,6 +2083,15 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
     if !self.headers.isEmpty {
       try visitor.visitRepeatedMessageField(value: self.headers, fieldNumber: 4)
     }
+    if !self.deviceID.isEmpty {
+      try visitor.visitSingularStringField(value: self.deviceID, fieldNumber: 5)
+    }
+    if !self.deviceOs.isEmpty {
+      try visitor.visitSingularStringField(value: self.deviceOs, fieldNumber: 6)
+    }
+    if !self.coderDesktopVersion.isEmpty {
+      try visitor.visitSingularStringField(value: self.coderDesktopVersion, fieldNumber: 7)
+    }
     try unknownFields.traverse(visitor: &visitor)
   }
 
@@ -1688,6 +2100,9 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
     if lhs.coderURL != rhs.coderURL {return false}
     if lhs.apiToken != rhs.apiToken {return false}
     if lhs.headers != rhs.headers {return false}
+    if lhs.deviceID != rhs.deviceID {return false}
+    if lhs.deviceOs != rhs.deviceOs {return false}
+    if lhs.coderDesktopVersion != rhs.coderDesktopVersion {return false}
     if lhs.unknownFields != rhs.unknownFields {return false}
     return true
   }
@@ -1825,3 +2240,80 @@ extension Vpn_StopResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
     return true
   }
 }
+
+extension Vpn_StatusRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  public static let protoMessageName: String = _protobuf_package + ".StatusRequest"
+  public static let _protobuf_nameMap = SwiftProtobuf._NameMap()
+
+  public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    // Load everything into unknown fields
+    while try decoder.nextFieldNumber() != nil {}
+  }
+
+  public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  public static func ==(lhs: Vpn_StatusRequest, rhs: Vpn_StatusRequest) -> Bool {
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Vpn_Status: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  public static let protoMessageName: String = _protobuf_package + ".Status"
+  public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "lifecycle"),
+    2: .standard(proto: "error_message"),
+    3: .standard(proto: "peer_update"),
+  ]
+
+  public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularEnumField(value: &self.lifecycle) }()
+      case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }()
+      case 3: try { try decoder.decodeSingularMessageField(value: &self._peerUpdate) }()
+      default: break
+      }
+    }
+  }
+
+  public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    if self.lifecycle != .unknown {
+      try visitor.visitSingularEnumField(value: self.lifecycle, fieldNumber: 1)
+    }
+    if !self.errorMessage.isEmpty {
+      try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2)
+    }
+    try { if let v = self._peerUpdate {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
+    } }()
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  public static func ==(lhs: Vpn_Status, rhs: Vpn_Status) -> Bool {
+    if lhs.lifecycle != rhs.lifecycle {return false}
+    if lhs.errorMessage != rhs.errorMessage {return false}
+    if lhs._peerUpdate != rhs._peerUpdate {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Vpn_Status.Lifecycle: SwiftProtobuf._ProtoNameProviding {
+  public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    0: .same(proto: "UNKNOWN"),
+    1: .same(proto: "STARTING"),
+    2: .same(proto: "STARTED"),
+    3: .same(proto: "STOPPING"),
+    4: .same(proto: "STOPPED"),
+  ]
+}
diff --git a/Coder-Desktop/VPNLib/vpn.proto b/Coder-Desktop/VPNLib/vpn.proto
index 9d9c2435..b3fe54c5 100644
--- a/Coder-Desktop/VPNLib/vpn.proto
+++ b/Coder-Desktop/VPNLib/vpn.proto
@@ -44,6 +44,26 @@ message TunnelMessage {
     }
 }
 
+// ClientMessage is a message from the client (to the service). Windows only.
+message ClientMessage {
+    RPC rpc = 1;
+    oneof msg {
+        StartRequest start = 2;
+        StopRequest stop = 3;
+        StatusRequest status = 4;
+    }
+}
+
+// ServiceMessage is a message from the service (to the client). Windows only.
+message ServiceMessage {
+    RPC rpc = 1;
+    oneof msg {
+        StartResponse start = 2;
+        StopResponse stop = 3;
+        Status status = 4; // either in reply to a StatusRequest or broadcasted
+    }
+}
+
 // Log is a log message generated by the tunnel.  The manager should log it to the system log. It is
 // one-way tunnel -> manager with no response.
 message Log {
@@ -185,6 +205,12 @@ message StartRequest {
         string value = 2;
     }
     repeated Header headers = 4;
+    // Device ID from Coder Desktop
+    string device_id = 5;
+    // Device OS from Coder Desktop
+    string device_os = 6;
+    // Coder Desktop version
+    string coder_desktop_version = 7;
 }
 
 message StartResponse {
@@ -202,3 +228,26 @@ message StopResponse {
     bool success = 1;
     string error_message = 2;
 }
+
+// StatusRequest is a request to get the status of the tunnel. The manager
+// replies with a Status.
+message StatusRequest {}
+
+// Status is sent in response to a StatusRequest or broadcasted to all clients
+// when the status changes.
+message Status {
+    enum Lifecycle {
+        UNKNOWN = 0;
+        STARTING = 1;
+        STARTED = 2;
+        STOPPING = 3;
+        STOPPED = 4;
+    }
+    Lifecycle lifecycle = 1;
+    string error_message = 2;
+
+    // This will be a FULL update with all workspaces and agents, so clients
+    // should replace their current peer state. Only the Upserted fields will
+    // be populated.
+    PeerUpdate peer_update = 3;
+}
diff --git a/Coder-Desktop/VPNLibTests/SpeakerTests.swift b/Coder-Desktop/VPNLibTests/SpeakerTests.swift
index fd8ffb76..dd837d70 100644
--- a/Coder-Desktop/VPNLibTests/SpeakerTests.swift
+++ b/Coder-Desktop/VPNLibTests/SpeakerTests.swift
@@ -29,14 +29,15 @@ struct SpeakerTests: Sendable {
         handshaker = Handshaker(
             writeFD: pipeMT.fileHandleForWriting,
             dispatch: dispatch, queue: queue,
-            role: .manager
+            role: .manager,
+            versions: [ProtoVersion(1, 1)]
         )
     }
 
     @Test func handshake() async throws {
         async let v = handshaker.handshake()
         try await uut.handshake()
-        #expect(try await v == ProtoVersion(1, 0))
+        #expect(try await v == ProtoVersion(1, 1))
     }
 
     @Test func handleSingleMessage() async throws {
diff --git a/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift
new file mode 100644
index 00000000..becf6b37
--- /dev/null
+++ b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift
@@ -0,0 +1,25 @@
+import Testing
+@testable import VPNLib
+
+@Suite(.timeLimit(.minutes(1)))
+struct TelemetryEnricherTests {
+    @Test func testEnrichStartRequest() throws {
+        let enricher0 = TelemetryEnricher()
+        let original = Vpn_StartRequest.with { req in
+            req.coderURL = "https://example.com"
+            req.tunnelFileDescriptor = 123
+        }
+        var enriched = enricher0.enrich(original)
+        #expect(enriched.coderURL == "https://example.com")
+        #expect(enriched.tunnelFileDescriptor == 123)
+        #expect(enriched.deviceOs == "macOS")
+        #expect(try enriched.coderDesktopVersion.contains(Regex(#"^\d+\.\d+\.\d+$"#)))
+        let deviceID = enriched.deviceID
+        #expect(!deviceID.isEmpty)
+
+        // check we get the same deviceID from a new enricher
+        let enricher1 = TelemetryEnricher()
+        enriched = enricher1.enrich(original)
+        #expect(enriched.deviceID == deviceID)
+    }
+}

From d95289b7513ffc9039d96fa43b3681f43cf483ca Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Thu, 27 Mar 2025 18:36:32 +1100
Subject: [PATCH 15/25] chore: update nix flake to include xcbeautify 2.27.0
 (#125)

---
 flake.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index 011c0d0a..03f26c8c 100644
--- a/flake.lock
+++ b/flake.lock
@@ -81,11 +81,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1740560979,
-        "narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=",
+        "lastModified": 1742889210,
+        "narHash": "sha256-hw63HnwnqU3ZQfsMclLhMvOezpM7RSB0dMAtD5/sOiw=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "5135c59491985879812717f4c9fea69604e7f26f",
+        "rev": "698214a32beb4f4c8e3942372c694f40848b360d",
         "type": "github"
       },
       "original": {

From f0cf155625f2f2ee585c3d834a935cc5629fd621 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Fri, 28 Mar 2025 13:47:09 +1100
Subject: [PATCH 16/25] feat: add stubbed file sync UI (#116)

Closes #66
Relates to #63


The UI differs a fair bit from the wireframes & figma designs in the interest of being able to use the stock SwiftUI Table view. The biggest difference is that a modal is used to insert new file syncs, as opposed to creating them inline. This was done as it's a lot harder to do that within a SwiftUI table. This design is also consistent with tables used in Apple's own settings pages, and the HTTP header table in app settings.


https://github.com/user-attachments/assets/7c3d98b9-36c4-430b-ac6f-7064b6b8dc31

The UI is mostly non-functional, it still needs to be wired up over gRPC, including conversions from Mutagen data types.
As a result, the file sync button on the menu will not appear unless the file sync feature flag is enabled in settings.

Right now, the workspace dropdown menu is populated from the online agents (any row with a coloured dot on the menubar menu)

There's no tests for this since ViewInspector still does not support Tables.
---
 .../Coder-Desktop/Coder_DesktopApp.swift      |   9 +-
 ...ntroller.swift => MenuBarController.swift} |   0
 .../Preview Content/PreviewFileSync.swift     |  24 ++++
 .../Coder-Desktop/VPN/MenuState.swift         |   6 +-
 .../Views/FileSync/FileSyncConfig.swift       | 118 ++++++++++++++++++
 .../Views/FileSync/FileSyncSessionModal.swift | 100 +++++++++++++++
 .../Coder-Desktop/Views/LoginForm.swift       |   6 +-
 .../Settings/LiteralHeadersSection.swift      |   4 +-
 .../Coder-Desktop/Views/StatusDot.swift       |  16 +++
 .../Views/{ => VPN}/Agents.swift              |   0
 .../Views/{ => VPN}/InvalidAgents.swift       |   0
 .../Views/{ => VPN}/VPNMenu.swift             |  25 +++-
 .../Views/{ => VPN}/VPNMenuItem.swift         |   9 +-
 .../Views/{ => VPN}/VPNState.swift            |   0
 Coder-Desktop/Coder-Desktop/Windows.swift     |   1 +
 Coder-Desktop/Coder-DesktopTests/Util.swift   |  24 ++++
 .../Coder-DesktopTests/VPNMenuTests.swift     |   8 +-
 .../VPNLib/FileSync/FileSyncDaemon.swift      |  43 +++----
 .../VPNLib/FileSync/FileSyncSession.swift     |  66 ++++++++++
 19 files changed, 415 insertions(+), 44 deletions(-)
 rename Coder-Desktop/Coder-Desktop/{MenuBarIconController.swift => MenuBarController.swift} (100%)
 create mode 100644 Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift
 create mode 100644 Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
 create mode 100644 Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
 create mode 100644 Coder-Desktop/Coder-Desktop/Views/StatusDot.swift
 rename Coder-Desktop/Coder-Desktop/Views/{ => VPN}/Agents.swift (100%)
 rename Coder-Desktop/Coder-Desktop/Views/{ => VPN}/InvalidAgents.swift (100%)
 rename Coder-Desktop/Coder-Desktop/Views/{ => VPN}/VPNMenu.swift (80%)
 rename Coder-Desktop/Coder-Desktop/Views/{ => VPN}/VPNMenuItem.swift (91%)
 rename Coder-Desktop/Coder-Desktop/Views/{ => VPN}/VPNState.swift (100%)
 create mode 100644 Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift

diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index 29b0910c..a110432d 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -23,6 +23,12 @@ struct DesktopApp: App {
                 .environmentObject(appDelegate.state)
         }
         .windowResizability(.contentSize)
+        Window("Coder File Sync", id: Windows.fileSync.rawValue) {
+            FileSyncConfig<CoderVPNService, MutagenDaemon>()
+                .environmentObject(appDelegate.state)
+                .environmentObject(appDelegate.fileSyncDaemon)
+                .environmentObject(appDelegate.vpn)
+        }
     }
 }
 
@@ -61,9 +67,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
                     await self.state.handleTokenExpiry()
                 }
             }, content: {
-                VPNMenu<CoderVPNService>().frame(width: 256)
+                VPNMenu<CoderVPNService, MutagenDaemon>().frame(width: 256)
                     .environmentObject(self.vpn)
                     .environmentObject(self.state)
+                    .environmentObject(self.fileSyncDaemon)
             }
         ))
         // Subscribe to system VPN updates
diff --git a/Coder-Desktop/Coder-Desktop/MenuBarIconController.swift b/Coder-Desktop/Coder-Desktop/MenuBarController.swift
similarity index 100%
rename from Coder-Desktop/Coder-Desktop/MenuBarIconController.swift
rename to Coder-Desktop/Coder-Desktop/MenuBarController.swift
diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift
new file mode 100644
index 00000000..8db30e3c
--- /dev/null
+++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
@@ -0,0 +1,24 @@
+import VPNLib
+
+@MainActor
+final class PreviewFileSync: FileSyncDaemon {
+    var sessionState: [VPNLib.FileSyncSession] = []
+
+    var state: DaemonState = .running
+
+    init() {}
+
+    func refreshSessions() async {}
+
+    func start() async throws(DaemonError) {
+        state = .running
+    }
+
+    func stop() async {
+        state = .stopped
+    }
+
+    func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
+
+    func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
+}
diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
index 69817e89..9c15aca3 100644
--- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
+++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
@@ -2,7 +2,7 @@ import Foundation
 import SwiftUI
 import VPNLib
 
-struct Agent: Identifiable, Equatable, Comparable {
+struct Agent: Identifiable, Equatable, Comparable, Hashable {
     let id: UUID
     let name: String
     let status: AgentStatus
@@ -135,6 +135,10 @@ struct VPNMenuState {
         return items.sorted()
     }
 
+    var onlineAgents: [Agent] {
+        agents.map(\.value).filter { $0.primaryHost != nil }
+    }
+
     mutating func clear() {
         agents.removeAll()
         workspaces.removeAll()
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
new file mode 100644
index 00000000..eb3065b8
--- /dev/null
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
@@ -0,0 +1,118 @@
+import SwiftUI
+import VPNLib
+
+struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
+    @EnvironmentObject var vpn: VPN
+    @EnvironmentObject var fileSync: FS
+
+    @State private var selection: FileSyncSession.ID?
+    @State private var addingNewSession: Bool = false
+    @State private var editingSession: FileSyncSession?
+
+    @State private var loading: Bool = false
+    @State private var deleteError: DaemonError?
+
+    var body: some View {
+        Group {
+            Table(fileSync.sessionState, selection: $selection) {
+                TableColumn("Local Path") {
+                    Text($0.alphaPath).help($0.alphaPath)
+                }.width(min: 200, ideal: 240)
+                TableColumn("Workspace", value: \.agentHost)
+                    .width(min: 100, ideal: 120)
+                TableColumn("Remote Path", value: \.betaPath)
+                    .width(min: 100, ideal: 120)
+                TableColumn("Status") { $0.status.body }
+                    .width(min: 80, ideal: 100)
+                TableColumn("Size") { item in
+                    Text(item.size)
+                }
+                .width(min: 60, ideal: 80)
+            }
+            .contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { _ in },
+                         primaryAction: { selectedSessions in
+                             if let session = selectedSessions.first {
+                                 editingSession = fileSync.sessionState.first(where: { $0.id == session })
+                             }
+                         })
+            .frame(minWidth: 400, minHeight: 200)
+            .padding(.bottom, 25)
+            .overlay(alignment: .bottom) {
+                VStack(alignment: .leading, spacing: 0) {
+                    Divider()
+                    HStack(spacing: 0) {
+                        Button {
+                            addingNewSession = true
+                        } label: {
+                            Image(systemName: "plus")
+                                .frame(width: 24, height: 24)
+                        }.disabled(vpn.menuState.agents.isEmpty)
+                        Divider()
+                        Button {
+                            Task {
+                                loading = true
+                                defer { loading = false }
+                                do throws(DaemonError) {
+                                    try await fileSync.deleteSessions(ids: [selection!])
+                                } catch {
+                                    deleteError = error
+                                }
+                                await fileSync.refreshSessions()
+                                selection = nil
+                            }
+                        } label: {
+                            Image(systemName: "minus").frame(width: 24, height: 24)
+                        }.disabled(selection == nil)
+                        if let selection {
+                            if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) {
+                                Divider()
+                                Button {
+                                    // TODO: Pause & Unpause
+                                } label: {
+                                    switch selectedSession.status {
+                                    case .paused:
+                                        Image(systemName: "play").frame(width: 24, height: 24)
+                                    default:
+                                        Image(systemName: "pause").frame(width: 24, height: 24)
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    .buttonStyle(.borderless)
+                }
+                .background(.primary.opacity(0.04))
+                .fixedSize(horizontal: false, vertical: true)
+            }
+        }.sheet(isPresented: $addingNewSession) {
+            FileSyncSessionModal<VPN, FS>()
+                .frame(width: 700)
+        }.sheet(item: $editingSession) { session in
+            FileSyncSessionModal<VPN, FS>(existingSession: session)
+                .frame(width: 700)
+        }.alert("Error", isPresented: Binding(
+            get: { deleteError != nil },
+            set: { isPresented in
+                if !isPresented {
+                    deleteError = nil
+                }
+            }
+        )) {} message: {
+            Text(deleteError?.description ?? "An unknown error occurred.")
+        }.task {
+            while !Task.isCancelled {
+                await fileSync.refreshSessions()
+                try? await Task.sleep(for: .seconds(2))
+            }
+        }.disabled(loading)
+    }
+}
+
+#if DEBUG
+    #Preview {
+        FileSyncConfig<PreviewVPN, PreviewFileSync>()
+            .environmentObject(AppState(persistent: false))
+            .environmentObject(PreviewVPN())
+            .environmentObject(PreviewFileSync())
+    }
+#endif
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
new file mode 100644
index 00000000..c0c7a35b
--- /dev/null
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
@@ -0,0 +1,100 @@
+import SwiftUI
+import VPNLib
+
+struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
+    var existingSession: FileSyncSession?
+    @Environment(\.dismiss) private var dismiss
+    @EnvironmentObject private var vpn: VPN
+    @EnvironmentObject private var fileSync: FS
+
+    @State private var localPath: String = ""
+    @State private var workspace: Agent?
+    @State private var remotePath: String = ""
+
+    @State private var loading: Bool = false
+    @State private var createError: DaemonError?
+
+    var body: some View {
+        let agents = vpn.menuState.onlineAgents
+        VStack(spacing: 0) {
+            Form {
+                Section {
+                    HStack(spacing: 5) {
+                        TextField("Local Path", text: $localPath)
+                        Spacer()
+                        Button {
+                            let panel = NSOpenPanel()
+                            panel.directoryURL = FileManager.default.homeDirectoryForCurrentUser
+                            panel.allowsMultipleSelection = false
+                            panel.canChooseDirectories = true
+                            panel.canChooseFiles = false
+                            if panel.runModal() == .OK {
+                                localPath = panel.url?.path(percentEncoded: false) ?? "<none>"
+                            }
+                        } label: {
+                            Image(systemName: "folder")
+                        }
+                    }
+                }
+                Section {
+                    Picker("Workspace", selection: $workspace) {
+                        ForEach(agents, id: \.id) { agent in
+                            Text(agent.primaryHost!).tag(agent)
+                        }
+                        // HACK: Silence error logs for no-selection.
+                        Divider().tag(nil as Agent?)
+                    }
+                }
+                Section {
+                    TextField("Remote Path", text: $remotePath)
+                }
+            }.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
+            Divider()
+            HStack {
+                Spacer()
+                Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
+                Button(existingSession == nil ? "Add" : "Save") { Task { await submit() }}
+                    .keyboardShortcut(.defaultAction)
+            }.padding(20)
+        }.onAppear {
+            if let existingSession {
+                localPath = existingSession.alphaPath
+                workspace = agents.first { $0.primaryHost == existingSession.agentHost }
+                remotePath = existingSession.betaPath
+            } else {
+                // Set the picker to the first agent by default
+                workspace = agents.first
+            }
+        }.disabled(loading)
+            .alert("Error", isPresented: Binding(
+                get: { createError != nil },
+                set: { if $0 { createError = nil } }
+            )) {} message: {
+                Text(createError?.description ?? "An unknown error occurred.")
+            }
+    }
+
+    func submit() async {
+        createError = nil
+        guard let workspace else {
+            return
+        }
+        loading = true
+        defer { loading = false }
+        do throws(DaemonError) {
+            if let existingSession {
+                // TODO: Support selecting & deleting multiple sessions at once
+                try await fileSync.deleteSessions(ids: [existingSession.id])
+            }
+            try await fileSync.createSession(
+                localPath: localPath,
+                agentHost: workspace.primaryHost!,
+                remotePath: remotePath
+            )
+        } catch {
+            createError = error
+            return
+        }
+        dismiss()
+    }
+}
diff --git a/Coder-Desktop/Coder-Desktop/Views/LoginForm.swift b/Coder-Desktop/Coder-Desktop/Views/LoginForm.swift
index 14b37f73..8b3d3a48 100644
--- a/Coder-Desktop/Coder-Desktop/Views/LoginForm.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/LoginForm.swift
@@ -48,10 +48,8 @@ struct LoginForm: View {
                     loginError = nil
                 }
             }
-        )) {
-            Button("OK", role: .cancel) {}.keyboardShortcut(.defaultAction)
-        } message: {
-            Text(loginError?.description ?? "")
+        )) {} message: {
+            Text(loginError?.description ?? "An unknown error occurred.")
         }.disabled(loading)
         .frame(width: 550)
         .fixedSize()
diff --git a/Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeadersSection.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeadersSection.swift
index e9a9b056..c0705c03 100644
--- a/Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeadersSection.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeadersSection.swift
@@ -15,7 +15,7 @@ struct LiteralHeadersSection<VPN: VPNService>: View {
             Toggle(isOn: $state.useLiteralHeaders) {
                 Text("HTTP Headers")
                 Text("When enabled, these headers will be included on all outgoing HTTP requests.")
-                if vpn.state != .disabled { Text("Cannot be modified while Coder Connect is enabled.") }
+                if !vpn.state.canBeStarted { Text("Cannot be modified while Coder Connect is enabled.") }
             }
             .controlSize(.large)
 
@@ -65,7 +65,7 @@ struct LiteralHeadersSection<VPN: VPNService>: View {
             LiteralHeaderModal(existingHeader: header)
         }.onTapGesture {
             selectedHeader = nil
-        }.disabled(vpn.state != .disabled)
+        }.disabled(!vpn.state.canBeStarted)
         .onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
     }
 }
diff --git a/Coder-Desktop/Coder-Desktop/Views/StatusDot.swift b/Coder-Desktop/Coder-Desktop/Views/StatusDot.swift
new file mode 100644
index 00000000..4de6041c
--- /dev/null
+++ b/Coder-Desktop/Coder-Desktop/Views/StatusDot.swift
@@ -0,0 +1,16 @@
+import SwiftUI
+
+struct StatusDot: View {
+    let color: Color
+
+    var body: some View {
+        ZStack {
+            Circle()
+                .fill(color.opacity(0.4))
+                .frame(width: 12, height: 12)
+            Circle()
+                .fill(color.opacity(1.0))
+                .frame(width: 7, height: 7)
+        }
+    }
+}
diff --git a/Coder-Desktop/Coder-Desktop/Views/Agents.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/Agents.swift
similarity index 100%
rename from Coder-Desktop/Coder-Desktop/Views/Agents.swift
rename to Coder-Desktop/Coder-Desktop/Views/VPN/Agents.swift
diff --git a/Coder-Desktop/Coder-Desktop/Views/InvalidAgents.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/InvalidAgents.swift
similarity index 100%
rename from Coder-Desktop/Coder-Desktop/Views/InvalidAgents.swift
rename to Coder-Desktop/Coder-Desktop/Views/VPN/InvalidAgents.swift
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
similarity index 80%
rename from Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift
rename to Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
index c3c44dba..b3fa74e2 100644
--- a/Coder-Desktop/Coder-Desktop/Views/VPNMenu.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
@@ -1,7 +1,9 @@
 import SwiftUI
+import VPNLib
 
-struct VPNMenu<VPN: VPNService>: View {
+struct VPNMenu<VPN: VPNService, FS: FileSyncDaemon>: View {
     @EnvironmentObject var vpn: VPN
+    @EnvironmentObject var fileSync: FS
     @EnvironmentObject var state: AppState
     @Environment(\.openSettings) private var openSettings
     @Environment(\.openWindow) private var openWindow
@@ -60,6 +62,24 @@ struct VPNMenu<VPN: VPNService>: View {
                     }.buttonStyle(.plain)
                     TrayDivider()
                 }
+                if vpn.state == .connected {
+                    Button {
+                        openWindow(id: .fileSync)
+                    } label: {
+                        ButtonRowView {
+                            HStack {
+                                // TODO: A future PR will provide users a way to recover from a daemon failure without
+                                // needing to restart the app
+                                if case .failed = fileSync.state, sessionsHaveError(fileSync.sessionState) {
+                                    Image(systemName: "exclamationmark.arrow.trianglehead.2.clockwise.rotate.90")
+                                        .frame(width: 12, height: 12).help("One or more sync sessions have errors")
+                                }
+                                Text("File sync")
+                            }
+                        }
+                    }.buttonStyle(.plain)
+                    TrayDivider()
+                }
                 if vpn.state == .failed(.systemExtensionError(.needsUserApproval)) {
                     Button {
                         openSystemExtensionSettings()
@@ -119,8 +139,9 @@ func openSystemExtensionSettings() {
         appState.login(baseAccessURL: URL(string: "http://127.0.0.1:8080")!, sessionToken: "")
         // appState.clearSession()
 
-        return VPNMenu<PreviewVPN>().frame(width: 256)
+        return VPNMenu<PreviewVPN, PreviewFileSync>().frame(width: 256)
             .environmentObject(PreviewVPN())
             .environmentObject(appState)
+            .environmentObject(PreviewFileSync())
     }
 #endif
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPNMenuItem.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
similarity index 91%
rename from Coder-Desktop/Coder-Desktop/Views/VPNMenuItem.swift
rename to Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
index d66150e5..af7e6bb8 100644
--- a/Coder-Desktop/Coder-Desktop/Views/VPNMenuItem.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
@@ -70,14 +70,7 @@ struct MenuItemView: View {
         HStack(spacing: 0) {
             Link(destination: wsURL) {
                 HStack(spacing: Theme.Size.trayPadding) {
-                    ZStack {
-                        Circle()
-                            .fill(item.status.color.opacity(0.4))
-                            .frame(width: 12, height: 12)
-                        Circle()
-                            .fill(item.status.color.opacity(1.0))
-                            .frame(width: 7, height: 7)
-                    }
+                    StatusDot(color: item.status.color)
                     Text(itemName).lineLimit(1).truncationMode(.tail)
                     Spacer()
                 }.padding(.horizontal, Theme.Size.trayPadding)
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPNState.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNState.swift
similarity index 100%
rename from Coder-Desktop/Coder-Desktop/Views/VPNState.swift
rename to Coder-Desktop/Coder-Desktop/Views/VPN/VPNState.swift
diff --git a/Coder-Desktop/Coder-Desktop/Windows.swift b/Coder-Desktop/Coder-Desktop/Windows.swift
index 61ac4ef6..24a5a9cc 100644
--- a/Coder-Desktop/Coder-Desktop/Windows.swift
+++ b/Coder-Desktop/Coder-Desktop/Windows.swift
@@ -3,6 +3,7 @@ import SwiftUI
 // Window IDs
 enum Windows: String {
     case login
+    case fileSync
 }
 
 extension OpenWindowAction {
diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
index c41f5c19..e38fe330 100644
--- a/Coder-Desktop/Coder-DesktopTests/Util.swift
+++ b/Coder-Desktop/Coder-DesktopTests/Util.swift
@@ -3,6 +3,7 @@ import Combine
 import NetworkExtension
 import SwiftUI
 import ViewInspector
+import VPNLib
 
 @MainActor
 class MockVPNService: VPNService, ObservableObject {
@@ -26,4 +27,27 @@ class MockVPNService: VPNService, ObservableObject {
     var startWhenReady: Bool = false
 }
 
+@MainActor
+class MockFileSyncDaemon: FileSyncDaemon {
+    var sessionState: [VPNLib.FileSyncSession] = []
+
+    func refreshSessions() async {}
+
+    func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
+
+    var state: VPNLib.DaemonState = .running
+
+    func start() async throws(VPNLib.DaemonError) {
+        return
+    }
+
+    func stop() async {}
+
+    func listSessions() async throws -> [VPNLib.FileSyncSession] {
+        []
+    }
+
+    func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
+}
+
 extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}
diff --git a/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift
index 616e3c53..46c780ca 100644
--- a/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift
@@ -7,15 +7,17 @@ import ViewInspector
 @Suite(.timeLimit(.minutes(1)))
 struct VPNMenuTests {
     let vpn: MockVPNService
+    let fsd: MockFileSyncDaemon
     let state: AppState
-    let sut: VPNMenu<MockVPNService>
+    let sut: VPNMenu<MockVPNService, MockFileSyncDaemon>
     let view: any View
 
     init() {
         vpn = MockVPNService()
         state = AppState(persistent: false)
-        sut = VPNMenu<MockVPNService>()
-        view = sut.environmentObject(vpn).environmentObject(state)
+        sut = VPNMenu<MockVPNService, MockFileSyncDaemon>()
+        fsd = MockFileSyncDaemon()
+        view = sut.environmentObject(vpn).environmentObject(state).environmentObject(fsd)
     }
 
     @Test
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
index 68446940..00633744 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
@@ -9,19 +9,12 @@ import SwiftUI
 @MainActor
 public protocol FileSyncDaemon: ObservableObject {
     var state: DaemonState { get }
+    var sessionState: [FileSyncSession] { get }
     func start() async throws(DaemonError)
     func stop() async
-    func listSessions() async throws -> [FileSyncSession]
-    func createSession(with: FileSyncSession) async throws
-}
-
-public struct FileSyncSession {
-    public let id: String
-    public let name: String
-    public let localPath: URL
-    public let workspace: String
-    public let agent: String
-    public let remotePath: URL
+    func refreshSessions() async
+    func createSession(localPath: String, agentHost: String, remotePath: String) async throws(DaemonError)
+    func deleteSessions(ids: [String]) async throws(DaemonError)
 }
 
 @MainActor
@@ -41,6 +34,8 @@ public class MutagenDaemon: FileSyncDaemon {
         }
     }
 
+    @Published public var sessionState: [FileSyncSession] = []
+
     private var mutagenProcess: Subprocess?
     private let mutagenPath: URL!
     private let mutagenDataDirectory: URL
@@ -79,7 +74,7 @@ public class MutagenDaemon: FileSyncDaemon {
                 state = .failed(error)
                 return
             }
-            await stopIfNoSessions()
+            await refreshSessions()
         }
     }
 
@@ -227,6 +222,7 @@ public class MutagenDaemon: FileSyncDaemon {
         let process = Subprocess([mutagenPath.path, "daemon", "run"])
         process.environment = [
             "MUTAGEN_DATA_DIRECTORY": mutagenDataDirectory.path,
+            "MUTAGEN_SSH_PATH": "/usr/bin",
         ]
         logger.info("setting mutagen data directory: \(self.mutagenDataDirectory.path, privacy: .public)")
         return process
@@ -256,27 +252,28 @@ public class MutagenDaemon: FileSyncDaemon {
         }
     }
 
-    public func listSessions() async throws -> [FileSyncSession] {
-        guard case .running = state else {
-            return []
-        }
+    public func refreshSessions() async {
+        guard case .running = state else { return }
         // TODO: Implement
-        return []
     }
 
-    public func createSession(with _: FileSyncSession) async throws {
+    public func createSession(
+        localPath _: String,
+        agentHost _: String,
+        remotePath _: String
+    ) async throws(DaemonError) {
         if case .stopped = state {
             do throws(DaemonError) {
                 try await start()
             } catch {
                 state = .failed(error)
-                return
+                throw error
             }
         }
-        // TODO: Add Session
+        // TODO: Add session
     }
 
-    public func deleteSession() async throws {
+    public func deleteSessions(ids _: [String]) async throws(DaemonError) {
         // TODO: Delete session
         await stopIfNoSessions()
     }
@@ -346,7 +343,7 @@ public enum DaemonError: Error {
     case terminatedUnexpectedly
     case grpcFailure(Error)
 
-    var description: String {
+    public var description: String {
         switch self {
         case let .daemonStartFailure(error):
             "Daemon start failure: \(error)"
@@ -361,5 +358,5 @@ public enum DaemonError: Error {
         }
     }
 
-    var localizedDescription: String { description }
+    public var localizedDescription: String { description }
 }
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift
new file mode 100644
index 00000000..e251b1a5
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift
@@ -0,0 +1,66 @@
+import SwiftUI
+
+public struct FileSyncSession: Identifiable {
+    public let id: String
+    public let alphaPath: String
+    public let agentHost: String
+    public let betaPath: String
+    public let status: FileSyncStatus
+    public let size: String
+}
+
+public enum FileSyncStatus {
+    case unknown
+    case error(String)
+    case ok
+    case paused
+    case needsAttention(String)
+    case working(String)
+
+    public var color: Color {
+        switch self {
+        case .ok:
+            .white
+        case .paused:
+            .secondary
+        case .unknown:
+            .red
+        case .error:
+            .red
+        case .needsAttention:
+            .orange
+        case .working:
+            .white
+        }
+    }
+
+    public var description: String {
+        switch self {
+        case .unknown:
+            "Unknown"
+        case let .error(msg):
+            msg
+        case .ok:
+            "Watching"
+        case .paused:
+            "Paused"
+        case let .needsAttention(msg):
+            msg
+        case let .working(msg):
+            msg
+        }
+    }
+
+    public var body: some View {
+        Text(description).foregroundColor(color)
+    }
+}
+
+public func sessionsHaveError(_ sessions: [FileSyncSession]) -> Bool {
+    for session in sessions {
+        if case .error = session.status {
+            return true
+        }
+    }
+    return false
+}

From 185a894bac6ce88bbac82c4126ab1d901e511af5 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Fri, 28 Mar 2025 13:50:21 +1100
Subject: [PATCH 17/25] chore: add mutagen session state conversion (#117)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Relates to #63.

This allows a Mutagen `Synchronization/State` message to be displayed as a single row in the table.

Just like on the Windows app, extra details are shown on hover. Though all columns can be expanded by dragging the column separators, full paths will also be shown on hover.

<img width="905" alt="Screenshot 2025-03-20 at 6 20 10 pm" src="https://github.com/user-attachments/assets/c3a51296-54e3-48c7-b3f6-9c915ba1b1a0" />
<img width="903" alt="Screenshot 2025-03-20 at 6 21 48 pm" src="https://github.com/user-attachments/assets/2cf9cd9f-6349-42f2-abcd-b354126aafda" />
<img width="906" alt="Screenshot 2025-03-20 at 6 21 40 pm" src="https://github.com/user-attachments/assets/ab6a50f5-9018-4712-be8c-9f6ad3ce44d0" />
<img width="906" alt="Screenshot 2025-03-20 at 6 20 18 pm" src="https://github.com/user-attachments/assets/7d5bb734-4a40-4897-a7ac-86513501b1ce" />
<img width="904" alt="image" src="https://github.com/user-attachments/assets/fdb1f2f8-7b70-4a78-9fb9-ac84df4ca5af" />
---
 .../Views/FileSync/FileSyncConfig.swift       |  10 +-
 .../VPNLib/FileSync/FileSyncSession.swift     | 269 +++++++++++++++++-
 .../VPNLib/FileSync/MutagenConvert.swift      |  59 ++++
 .../{Convert.swift => VPNConvert.swift}       |   0
 4 files changed, 317 insertions(+), 21 deletions(-)
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift
 rename Coder-Desktop/VPNLib/{Convert.swift => VPNConvert.swift} (100%)

diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
index eb3065b8..dc83c17a 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
@@ -20,14 +20,12 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
                 }.width(min: 200, ideal: 240)
                 TableColumn("Workspace", value: \.agentHost)
                     .width(min: 100, ideal: 120)
-                TableColumn("Remote Path", value: \.betaPath)
+                TableColumn("Remote Path") { Text($0.betaPath).help($0.betaPath) }
                     .width(min: 100, ideal: 120)
-                TableColumn("Status") { $0.status.body }
+                TableColumn("Status") { $0.status.column.help($0.statusAndErrors) }
                     .width(min: 80, ideal: 100)
-                TableColumn("Size") { item in
-                    Text(item.size)
-                }
-                .width(min: 60, ideal: 80)
+                TableColumn("Size") { Text($0.localSize.humanSizeBytes).help($0.sizeDescription) }
+                    .width(min: 60, ideal: 80)
             }
             .contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { _ in },
                          primaryAction: { selectedSessions in
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift
index e251b1a5..d586908d 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift
@@ -3,19 +3,126 @@ import SwiftUI
 public struct FileSyncSession: Identifiable {
     public let id: String
     public let alphaPath: String
+    public let name: String
+
     public let agentHost: String
     public let betaPath: String
     public let status: FileSyncStatus
-    public let size: String
+
+    public let localSize: FileSyncSessionEndpointSize
+    public let remoteSize: FileSyncSessionEndpointSize
+
+    public let errors: [FileSyncError]
+
+    init(state: Synchronization_State) {
+        id = state.session.identifier
+        name = state.session.name
+
+        // If the protocol isn't what we expect for alpha or beta, show unknown
+        alphaPath = if state.session.alpha.protocol == Url_Protocol.local, !state.session.alpha.path.isEmpty {
+            state.session.alpha.path
+        } else {
+            "Unknown"
+        }
+        agentHost = if state.session.beta.protocol == Url_Protocol.ssh, !state.session.beta.host.isEmpty {
+            // TOOD: We need to either:
+            // - make this compatible with custom suffixes
+            // - always strip the tld
+            // - always keep the tld
+            state.session.beta.host
+        } else {
+            "Unknown"
+        }
+        betaPath = if !state.session.beta.path.isEmpty {
+            state.session.beta.path
+        } else {
+            "Unknown"
+        }
+
+        var status: FileSyncStatus = if state.session.paused {
+            .paused
+        } else {
+            convertSessionStatus(status: state.status)
+        }
+        if case .error = status {} else {
+            if state.conflicts.count > 0 {
+                status = .conflicts
+            }
+        }
+        self.status = status
+
+        localSize = .init(
+            sizeBytes: state.alphaState.totalFileSize,
+            fileCount: state.alphaState.files,
+            dirCount: state.alphaState.directories,
+            symLinkCount: state.alphaState.symbolicLinks
+        )
+        remoteSize = .init(
+            sizeBytes: state.betaState.totalFileSize,
+            fileCount: state.betaState.files,
+            dirCount: state.betaState.directories,
+            symLinkCount: state.betaState.symbolicLinks
+        )
+
+        errors = accumulateErrors(from: state)
+    }
+
+    public var statusAndErrors: String {
+        var out = "\(status.type)\n\n\(status.description)"
+        errors.forEach { out += "\n\t\($0)" }
+        return out
+    }
+
+    public var sizeDescription: String {
+        var out = ""
+        out += "Local:\n\(localSize.description(linePrefix: " "))\n\n"
+        out += "Remote:\n\(remoteSize.description(linePrefix: " "))"
+        return out
+    }
+}
+
+public struct FileSyncSessionEndpointSize: Equatable {
+    public let sizeBytes: UInt64
+    public let fileCount: UInt64
+    public let dirCount: UInt64
+    public let symLinkCount: UInt64
+
+    public init(sizeBytes: UInt64, fileCount: UInt64, dirCount: UInt64, symLinkCount: UInt64) {
+        self.sizeBytes = sizeBytes
+        self.fileCount = fileCount
+        self.dirCount = dirCount
+        self.symLinkCount = symLinkCount
+    }
+
+    public var humanSizeBytes: String {
+        humanReadableBytes(sizeBytes)
+    }
+
+    public func description(linePrefix: String = "") -> String {
+        var result = ""
+        result += linePrefix + humanReadableBytes(sizeBytes) + "\n"
+        let numberFormatter = NumberFormatter()
+        numberFormatter.numberStyle = .decimal
+        if let formattedFileCount = numberFormatter.string(from: NSNumber(value: fileCount)) {
+            result += "\(linePrefix)\(formattedFileCount) file\(fileCount == 1 ? "" : "s")\n"
+        }
+        if let formattedDirCount = numberFormatter.string(from: NSNumber(value: dirCount)) {
+            result += "\(linePrefix)\(formattedDirCount) director\(dirCount == 1 ? "y" : "ies")"
+        }
+        if symLinkCount > 0, let formattedSymLinkCount = numberFormatter.string(from: NSNumber(value: symLinkCount)) {
+            result += "\n\(linePrefix)\(formattedSymLinkCount) symlink\(symLinkCount == 1 ? "" : "s")"
+        }
+        return result
+    }
 }
 
 public enum FileSyncStatus {
     case unknown
-    case error(String)
+    case error(FileSyncErrorStatus)
     case ok
     case paused
-    case needsAttention(String)
-    case working(String)
+    case conflicts
+    case working(FileSyncWorkingStatus)
 
     public var color: Color {
         switch self {
@@ -27,32 +134,164 @@ public enum FileSyncStatus {
             .red
         case .error:
             .red
-        case .needsAttention:
+        case .conflicts:
             .orange
         case .working:
-            .white
+            .purple
         }
     }
 
-    public var description: String {
+    public var type: String {
         switch self {
         case .unknown:
             "Unknown"
-        case let .error(msg):
-            msg
+        case let .error(status):
+            status.name
         case .ok:
             "Watching"
         case .paused:
             "Paused"
-        case let .needsAttention(msg):
-            msg
-        case let .working(msg):
-            msg
+        case .conflicts:
+            "Conflicts"
+        case let .working(status):
+            status.name
+        }
+    }
+
+    public var description: String {
+        switch self {
+        case .unknown:
+            "Unknown status message."
+        case let .error(status):
+            status.description
+        case .ok:
+            "The session is watching for filesystem changes."
+        case .paused:
+            "The session is paused."
+        case .conflicts:
+            "The session has conflicts that need to be resolved."
+        case let .working(status):
+            status.description
+        }
+    }
+
+    public var column: some View {
+        Text(type).foregroundColor(color)
+    }
+}
+
+public enum FileSyncWorkingStatus {
+    case connectingAlpha
+    case connectingBeta
+    case scanning
+    case reconciling
+    case stagingAlpha
+    case stagingBeta
+    case transitioning
+    case saving
+
+    var name: String {
+        switch self {
+        case .connectingAlpha:
+            "Connecting (alpha)"
+        case .connectingBeta:
+            "Connecting (beta)"
+        case .scanning:
+            "Scanning"
+        case .reconciling:
+            "Reconciling"
+        case .stagingAlpha:
+            "Staging (alpha)"
+        case .stagingBeta:
+            "Staging (beta)"
+        case .transitioning:
+            "Transitioning"
+        case .saving:
+            "Saving"
+        }
+    }
+
+    var description: String {
+        switch self {
+        case .connectingAlpha:
+            "The session is attempting to connect to the alpha endpoint."
+        case .connectingBeta:
+            "The session is attempting to connect to the beta endpoint."
+        case .scanning:
+            "The session is scanning the filesystem on each endpoint."
+        case .reconciling:
+            "The session is performing reconciliation."
+        case .stagingAlpha:
+            "The session is staging files on the alpha endpoint"
+        case .stagingBeta:
+            "The session is staging files on the beta endpoint"
+        case .transitioning:
+            "The session is performing transition operations on each endpoint."
+        case .saving:
+            "The session is recording synchronization history to disk."
         }
     }
+}
+
+public enum FileSyncErrorStatus {
+    case disconnected
+    case haltedOnRootEmptied
+    case haltedOnRootDeletion
+    case haltedOnRootTypeChange
+    case waitingForRescan
+
+    var name: String {
+        switch self {
+        case .disconnected:
+            "Disconnected"
+        case .haltedOnRootEmptied:
+            "Halted on root emptied"
+        case .haltedOnRootDeletion:
+            "Halted on root deletion"
+        case .haltedOnRootTypeChange:
+            "Halted on root type change"
+        case .waitingForRescan:
+            "Waiting for rescan"
+        }
+    }
+
+    var description: String {
+        switch self {
+        case .disconnected:
+            "The session is unpaused but not currently connected or connecting to either endpoint."
+        case .haltedOnRootEmptied:
+            "The session is halted due to the root emptying safety check."
+        case .haltedOnRootDeletion:
+            "The session is halted due to the root deletion safety check."
+        case .haltedOnRootTypeChange:
+            "The session is halted due to the root type change safety check."
+        case .waitingForRescan:
+            "The session is waiting to retry scanning after an error during the previous scan."
+        }
+    }
+}
 
-    public var body: some View {
-        Text(description).foregroundColor(color)
+public enum FileSyncEndpoint {
+    case local
+    case remote
+}
+
+public enum FileSyncProblemType {
+    case scan
+    case transition
+}
+
+public enum FileSyncError {
+    case generic(String)
+    case problem(FileSyncEndpoint, FileSyncProblemType, path: String, error: String)
+
+    var description: String {
+        switch self {
+        case let .generic(error):
+            error
+        case let .problem(endpoint, type, path, error):
+            "\(endpoint) \(type) error at \(path): \(error)"
+        }
     }
 }
 
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift
new file mode 100644
index 00000000..7afefee1
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift
@@ -0,0 +1,59 @@
+// swiftlint:disable:next cyclomatic_complexity
+func convertSessionStatus(status: Synchronization_Status) -> FileSyncStatus {
+    switch status {
+    case .disconnected:
+        .error(.disconnected)
+    case .haltedOnRootEmptied:
+        .error(.haltedOnRootEmptied)
+    case .haltedOnRootDeletion:
+        .error(.haltedOnRootDeletion)
+    case .haltedOnRootTypeChange:
+        .error(.haltedOnRootTypeChange)
+    case .waitingForRescan:
+        .error(.waitingForRescan)
+    case .connectingAlpha:
+        .working(.connectingAlpha)
+    case .connectingBeta:
+        .working(.connectingBeta)
+    case .scanning:
+        .working(.scanning)
+    case .reconciling:
+        .working(.reconciling)
+    case .stagingAlpha:
+        .working(.stagingAlpha)
+    case .stagingBeta:
+        .working(.stagingBeta)
+    case .transitioning:
+        .working(.transitioning)
+    case .saving:
+        .working(.saving)
+    case .watching:
+        .ok
+    case .UNRECOGNIZED:
+        .unknown
+    }
+}
+
+func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] {
+    var errors: [FileSyncError] = []
+    if !state.lastError.isEmpty {
+        errors.append(.generic(state.lastError))
+    }
+    for problem in state.alphaState.scanProblems {
+        errors.append(.problem(.local, .scan, path: problem.path, error: problem.error))
+    }
+    for problem in state.alphaState.transitionProblems {
+        errors.append(.problem(.local, .transition, path: problem.path, error: problem.error))
+    }
+    for problem in state.betaState.scanProblems {
+        errors.append(.problem(.remote, .scan, path: problem.path, error: problem.error))
+    }
+    for problem in state.betaState.transitionProblems {
+        errors.append(.problem(.remote, .transition, path: problem.path, error: problem.error))
+    }
+    return errors
+}
+
+func humanReadableBytes(_ bytes: UInt64) -> String {
+    ByteCountFormatter().string(fromByteCount: Int64(bytes))
+}
diff --git a/Coder-Desktop/VPNLib/Convert.swift b/Coder-Desktop/VPNLib/VPNConvert.swift
similarity index 100%
rename from Coder-Desktop/VPNLib/Convert.swift
rename to Coder-Desktop/VPNLib/VPNConvert.swift

From 2669a1c04ffce97f5c2bf529ab6d0c38fa6ceffc Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Fri, 28 Mar 2025 13:53:21 +1100
Subject: [PATCH 18/25] chore: add mutagen prompting gRPC (#118)

Relates to #63.

The daemon requires this prompting communication channel be open during all requests.
---
 .../VPNLib/FileSync/FileSyncDaemon.swift      |  14 +-
 .../VPNLib/FileSync/FileSyncPrompting.swift   |  53 +++
 .../VPNLib/FileSync/MutagenConvert.swift      |  23 +
 .../service_prompting_prompting.grpc.swift    | 421 ++++++++++++++++++
 .../service_prompting_prompting.pb.swift      | 279 ++++++++++++
 .../service_prompting_prompting.proto         |  80 ++++
 scripts/mutagen-proto.sh                      |  32 +-
 7 files changed, 883 insertions(+), 19 deletions(-)
 create mode 100644 Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.grpc.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.pb.swift
 create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.proto

diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
index 00633744..eafd4dc7 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
@@ -19,7 +19,7 @@ public protocol FileSyncDaemon: ObservableObject {
 
 @MainActor
 public class MutagenDaemon: FileSyncDaemon {
-    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen")
+    let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen")
 
     @Published public var state: DaemonState = .stopped {
         didSet {
@@ -42,9 +42,9 @@ public class MutagenDaemon: FileSyncDaemon {
     private let mutagenDaemonSocket: URL
 
     // Non-nil when the daemon is running
+    var client: DaemonClient?
     private var group: MultiThreadedEventLoopGroup?
     private var channel: GRPCChannel?
-    private var client: DaemonClient?
 
     // Protect start & stop transitions against re-entrancy
     private let transition = AsyncSemaphore(value: 1)
@@ -171,7 +171,8 @@ public class MutagenDaemon: FileSyncDaemon {
             )
             client = DaemonClient(
                 mgmt: Daemon_DaemonAsyncClient(channel: channel!),
-                sync: Synchronization_SynchronizationAsyncClient(channel: channel!)
+                sync: Synchronization_SynchronizationAsyncClient(channel: channel!),
+                prompt: Prompting_PromptingAsyncClient(channel: channel!)
             )
             logger.info(
                 "Successfully connected to mutagen daemon, socket: \(self.mutagenDaemonSocket.path, privacy: .public)"
@@ -301,6 +302,7 @@ public class MutagenDaemon: FileSyncDaemon {
 struct DaemonClient {
     let mgmt: Daemon_DaemonAsyncClient
     let sync: Synchronization_SynchronizationAsyncClient
+    let prompt: Prompting_PromptingAsyncClient
 }
 
 public enum DaemonState {
@@ -342,6 +344,8 @@ public enum DaemonError: Error {
     case connectionFailure(Error)
     case terminatedUnexpectedly
     case grpcFailure(Error)
+    case invalidGrpcResponse(String)
+    case unexpectedStreamClosure
 
     public var description: String {
         switch self {
@@ -355,6 +359,10 @@ public enum DaemonError: Error {
             "The daemon must be started first"
         case let .grpcFailure(error):
             "Failed to communicate with daemon: \(error)"
+        case let .invalidGrpcResponse(response):
+            "Invalid gRPC response: \(response)"
+        case .unexpectedStreamClosure:
+            "Unexpected stream closure"
         }
     }
 
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift
new file mode 100644
index 00000000..d5a49b42
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift
@@ -0,0 +1,53 @@
+import GRPC
+
+extension MutagenDaemon {
+    typealias PromptStream = GRPCAsyncBidirectionalStreamingCall<Prompting_HostRequest, Prompting_HostResponse>
+
+    func host(allowPrompts: Bool = true) async throws(DaemonError) -> (PromptStream, identifier: String) {
+        let stream = client!.prompt.makeHostCall()
+
+        do {
+            try await stream.requestStream.send(.with { req in req.allowPrompts = allowPrompts })
+        } catch {
+            throw .grpcFailure(error)
+        }
+
+        // We can't make call `makeAsyncIterator` more than once
+        // (as a for-loop would do implicitly)
+        var iter = stream.responseStream.makeAsyncIterator()
+
+        let initResp: Prompting_HostResponse?
+        do {
+            initResp = try await iter.next()
+        } catch {
+            throw .grpcFailure(error)
+        }
+        guard let initResp else {
+            throw .unexpectedStreamClosure
+        }
+        try initResp.ensureValid(first: true, allowPrompts: allowPrompts)
+
+        Task.detached(priority: .background) {
+            do {
+                while let msg = try await iter.next() {
+                    try msg.ensureValid(first: false, allowPrompts: allowPrompts)
+                    var reply: Prompting_HostRequest = .init()
+                    if msg.isPrompt {
+                        // Handle SSH key prompts
+                        if msg.message.contains("yes/no/[fingerprint]") {
+                            reply.response = "yes"
+                        }
+                        // Any other messages that require a non-empty response will
+                        // cause the create op to fail, showing an error. This is ok for now.
+                    }
+                    try await stream.requestStream.send(reply)
+                }
+            } catch let error as GRPCStatus where error.code == .cancelled {
+                return
+            } catch {
+                self.logger.critical("Prompt stream failed: \(error)")
+            }
+        }
+        return (stream, identifier: initResp.identifier)
+    }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift
index 7afefee1..8a59b238 100644
--- a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift
@@ -57,3 +57,26 @@ func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] {
 func humanReadableBytes(_ bytes: UInt64) -> String {
     ByteCountFormatter().string(fromByteCount: Int64(bytes))
 }
+
+extension Prompting_HostResponse {
+    func ensureValid(first: Bool, allowPrompts: Bool) throws(DaemonError) {
+        if first {
+            if identifier.isEmpty {
+                throw .invalidGrpcResponse("empty prompter identifier")
+            }
+            if isPrompt {
+                throw .invalidGrpcResponse("unexpected message type specification")
+            }
+            if !message.isEmpty {
+                throw .invalidGrpcResponse("unexpected message")
+            }
+        } else {
+            if !identifier.isEmpty {
+                throw .invalidGrpcResponse("unexpected prompter identifier")
+            }
+            if isPrompt, !allowPrompts {
+                throw .invalidGrpcResponse("disallowed prompt message type")
+            }
+        }
+    }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.grpc.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.grpc.swift
new file mode 100644
index 00000000..a79eb510
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.grpc.swift
@@ -0,0 +1,421 @@
+//
+// DO NOT EDIT.
+// swift-format-ignore-file
+//
+// Generated by the protocol buffer compiler.
+// Source: service_prompting_prompting.proto
+//
+import GRPC
+import NIO
+import NIOConcurrencyHelpers
+import SwiftProtobuf
+
+
+/// Prompting allows clients to host and request prompting.
+///
+/// Usage: instantiate `Prompting_PromptingClient`, then call methods of this protocol to make API calls.
+internal protocol Prompting_PromptingClientProtocol: GRPCClient {
+  var serviceName: String { get }
+  var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? { get }
+
+  func host(
+    callOptions: CallOptions?,
+    handler: @escaping (Prompting_HostResponse) -> Void
+  ) -> BidirectionalStreamingCall<Prompting_HostRequest, Prompting_HostResponse>
+
+  func prompt(
+    _ request: Prompting_PromptRequest,
+    callOptions: CallOptions?
+  ) -> UnaryCall<Prompting_PromptRequest, Prompting_PromptResponse>
+}
+
+extension Prompting_PromptingClientProtocol {
+  internal var serviceName: String {
+    return "prompting.Prompting"
+  }
+
+  /// Host allows clients to perform prompt hosting.
+  ///
+  /// Callers should use the `send` method on the returned object to send messages
+  /// to the server. The caller should send an `.end` after the final message has been sent.
+  ///
+  /// - Parameters:
+  ///   - callOptions: Call options.
+  ///   - handler: A closure called when each response is received from the server.
+  /// - Returns: A `ClientStreamingCall` with futures for the metadata and status.
+  internal func host(
+    callOptions: CallOptions? = nil,
+    handler: @escaping (Prompting_HostResponse) -> Void
+  ) -> BidirectionalStreamingCall<Prompting_HostRequest, Prompting_HostResponse> {
+    return self.makeBidirectionalStreamingCall(
+      path: Prompting_PromptingClientMetadata.Methods.host.path,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeHostInterceptors() ?? [],
+      handler: handler
+    )
+  }
+
+  /// Prompt performs prompting using a specific prompter.
+  ///
+  /// - Parameters:
+  ///   - request: Request to send to Prompt.
+  ///   - callOptions: Call options.
+  /// - Returns: A `UnaryCall` with futures for the metadata, status and response.
+  internal func prompt(
+    _ request: Prompting_PromptRequest,
+    callOptions: CallOptions? = nil
+  ) -> UnaryCall<Prompting_PromptRequest, Prompting_PromptResponse> {
+    return self.makeUnaryCall(
+      path: Prompting_PromptingClientMetadata.Methods.prompt.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makePromptInterceptors() ?? []
+    )
+  }
+}
+
+@available(*, deprecated)
+extension Prompting_PromptingClient: @unchecked Sendable {}
+
+@available(*, deprecated, renamed: "Prompting_PromptingNIOClient")
+internal final class Prompting_PromptingClient: Prompting_PromptingClientProtocol {
+  private let lock = Lock()
+  private var _defaultCallOptions: CallOptions
+  private var _interceptors: Prompting_PromptingClientInterceptorFactoryProtocol?
+  internal let channel: GRPCChannel
+  internal var defaultCallOptions: CallOptions {
+    get { self.lock.withLock { return self._defaultCallOptions } }
+    set { self.lock.withLockVoid { self._defaultCallOptions = newValue } }
+  }
+  internal var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? {
+    get { self.lock.withLock { return self._interceptors } }
+    set { self.lock.withLockVoid { self._interceptors = newValue } }
+  }
+
+  /// Creates a client for the prompting.Prompting service.
+  ///
+  /// - Parameters:
+  ///   - channel: `GRPCChannel` to the service host.
+  ///   - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
+  ///   - interceptors: A factory providing interceptors for each RPC.
+  internal init(
+    channel: GRPCChannel,
+    defaultCallOptions: CallOptions = CallOptions(),
+    interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? = nil
+  ) {
+    self.channel = channel
+    self._defaultCallOptions = defaultCallOptions
+    self._interceptors = interceptors
+  }
+}
+
+internal struct Prompting_PromptingNIOClient: Prompting_PromptingClientProtocol {
+  internal var channel: GRPCChannel
+  internal var defaultCallOptions: CallOptions
+  internal var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol?
+
+  /// Creates a client for the prompting.Prompting service.
+  ///
+  /// - Parameters:
+  ///   - channel: `GRPCChannel` to the service host.
+  ///   - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
+  ///   - interceptors: A factory providing interceptors for each RPC.
+  internal init(
+    channel: GRPCChannel,
+    defaultCallOptions: CallOptions = CallOptions(),
+    interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? = nil
+  ) {
+    self.channel = channel
+    self.defaultCallOptions = defaultCallOptions
+    self.interceptors = interceptors
+  }
+}
+
+/// Prompting allows clients to host and request prompting.
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+internal protocol Prompting_PromptingAsyncClientProtocol: GRPCClient {
+  static var serviceDescriptor: GRPCServiceDescriptor { get }
+  var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? { get }
+
+  func makeHostCall(
+    callOptions: CallOptions?
+  ) -> GRPCAsyncBidirectionalStreamingCall<Prompting_HostRequest, Prompting_HostResponse>
+
+  func makePromptCall(
+    _ request: Prompting_PromptRequest,
+    callOptions: CallOptions?
+  ) -> GRPCAsyncUnaryCall<Prompting_PromptRequest, Prompting_PromptResponse>
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension Prompting_PromptingAsyncClientProtocol {
+  internal static var serviceDescriptor: GRPCServiceDescriptor {
+    return Prompting_PromptingClientMetadata.serviceDescriptor
+  }
+
+  internal var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? {
+    return nil
+  }
+
+  internal func makeHostCall(
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncBidirectionalStreamingCall<Prompting_HostRequest, Prompting_HostResponse> {
+    return self.makeAsyncBidirectionalStreamingCall(
+      path: Prompting_PromptingClientMetadata.Methods.host.path,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeHostInterceptors() ?? []
+    )
+  }
+
+  internal func makePromptCall(
+    _ request: Prompting_PromptRequest,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncUnaryCall<Prompting_PromptRequest, Prompting_PromptResponse> {
+    return self.makeAsyncUnaryCall(
+      path: Prompting_PromptingClientMetadata.Methods.prompt.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makePromptInterceptors() ?? []
+    )
+  }
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension Prompting_PromptingAsyncClientProtocol {
+  internal func host<RequestStream>(
+    _ requests: RequestStream,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncResponseStream<Prompting_HostResponse> where RequestStream: Sequence, RequestStream.Element == Prompting_HostRequest {
+    return self.performAsyncBidirectionalStreamingCall(
+      path: Prompting_PromptingClientMetadata.Methods.host.path,
+      requests: requests,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeHostInterceptors() ?? []
+    )
+  }
+
+  internal func host<RequestStream>(
+    _ requests: RequestStream,
+    callOptions: CallOptions? = nil
+  ) -> GRPCAsyncResponseStream<Prompting_HostResponse> where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Prompting_HostRequest {
+    return self.performAsyncBidirectionalStreamingCall(
+      path: Prompting_PromptingClientMetadata.Methods.host.path,
+      requests: requests,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makeHostInterceptors() ?? []
+    )
+  }
+
+  internal func prompt(
+    _ request: Prompting_PromptRequest,
+    callOptions: CallOptions? = nil
+  ) async throws -> Prompting_PromptResponse {
+    return try await self.performAsyncUnaryCall(
+      path: Prompting_PromptingClientMetadata.Methods.prompt.path,
+      request: request,
+      callOptions: callOptions ?? self.defaultCallOptions,
+      interceptors: self.interceptors?.makePromptInterceptors() ?? []
+    )
+  }
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+internal struct Prompting_PromptingAsyncClient: Prompting_PromptingAsyncClientProtocol {
+  internal var channel: GRPCChannel
+  internal var defaultCallOptions: CallOptions
+  internal var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol?
+
+  internal init(
+    channel: GRPCChannel,
+    defaultCallOptions: CallOptions = CallOptions(),
+    interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? = nil
+  ) {
+    self.channel = channel
+    self.defaultCallOptions = defaultCallOptions
+    self.interceptors = interceptors
+  }
+}
+
+internal protocol Prompting_PromptingClientInterceptorFactoryProtocol: Sendable {
+
+  /// - Returns: Interceptors to use when invoking 'host'.
+  func makeHostInterceptors() -> [ClientInterceptor<Prompting_HostRequest, Prompting_HostResponse>]
+
+  /// - Returns: Interceptors to use when invoking 'prompt'.
+  func makePromptInterceptors() -> [ClientInterceptor<Prompting_PromptRequest, Prompting_PromptResponse>]
+}
+
+internal enum Prompting_PromptingClientMetadata {
+  internal static let serviceDescriptor = GRPCServiceDescriptor(
+    name: "Prompting",
+    fullName: "prompting.Prompting",
+    methods: [
+      Prompting_PromptingClientMetadata.Methods.host,
+      Prompting_PromptingClientMetadata.Methods.prompt,
+    ]
+  )
+
+  internal enum Methods {
+    internal static let host = GRPCMethodDescriptor(
+      name: "Host",
+      path: "/prompting.Prompting/Host",
+      type: GRPCCallType.bidirectionalStreaming
+    )
+
+    internal static let prompt = GRPCMethodDescriptor(
+      name: "Prompt",
+      path: "/prompting.Prompting/Prompt",
+      type: GRPCCallType.unary
+    )
+  }
+}
+
+/// Prompting allows clients to host and request prompting.
+///
+/// To build a server, implement a class that conforms to this protocol.
+internal protocol Prompting_PromptingProvider: CallHandlerProvider {
+  var interceptors: Prompting_PromptingServerInterceptorFactoryProtocol? { get }
+
+  /// Host allows clients to perform prompt hosting.
+  func host(context: StreamingResponseCallContext<Prompting_HostResponse>) -> EventLoopFuture<(StreamEvent<Prompting_HostRequest>) -> Void>
+
+  /// Prompt performs prompting using a specific prompter.
+  func prompt(request: Prompting_PromptRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Prompting_PromptResponse>
+}
+
+extension Prompting_PromptingProvider {
+  internal var serviceName: Substring {
+    return Prompting_PromptingServerMetadata.serviceDescriptor.fullName[...]
+  }
+
+  /// Determines, calls and returns the appropriate request handler, depending on the request's method.
+  /// Returns nil for methods not handled by this service.
+  internal func handle(
+    method name: Substring,
+    context: CallHandlerContext
+  ) -> GRPCServerHandlerProtocol? {
+    switch name {
+    case "Host":
+      return BidirectionalStreamingServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Prompting_HostRequest>(),
+        responseSerializer: ProtobufSerializer<Prompting_HostResponse>(),
+        interceptors: self.interceptors?.makeHostInterceptors() ?? [],
+        observerFactory: self.host(context:)
+      )
+
+    case "Prompt":
+      return UnaryServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Prompting_PromptRequest>(),
+        responseSerializer: ProtobufSerializer<Prompting_PromptResponse>(),
+        interceptors: self.interceptors?.makePromptInterceptors() ?? [],
+        userFunction: self.prompt(request:context:)
+      )
+
+    default:
+      return nil
+    }
+  }
+}
+
+/// Prompting allows clients to host and request prompting.
+///
+/// To implement a server, implement an object which conforms to this protocol.
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+internal protocol Prompting_PromptingAsyncProvider: CallHandlerProvider, Sendable {
+  static var serviceDescriptor: GRPCServiceDescriptor { get }
+  var interceptors: Prompting_PromptingServerInterceptorFactoryProtocol? { get }
+
+  /// Host allows clients to perform prompt hosting.
+  func host(
+    requestStream: GRPCAsyncRequestStream<Prompting_HostRequest>,
+    responseStream: GRPCAsyncResponseStreamWriter<Prompting_HostResponse>,
+    context: GRPCAsyncServerCallContext
+  ) async throws
+
+  /// Prompt performs prompting using a specific prompter.
+  func prompt(
+    request: Prompting_PromptRequest,
+    context: GRPCAsyncServerCallContext
+  ) async throws -> Prompting_PromptResponse
+}
+
+@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
+extension Prompting_PromptingAsyncProvider {
+  internal static var serviceDescriptor: GRPCServiceDescriptor {
+    return Prompting_PromptingServerMetadata.serviceDescriptor
+  }
+
+  internal var serviceName: Substring {
+    return Prompting_PromptingServerMetadata.serviceDescriptor.fullName[...]
+  }
+
+  internal var interceptors: Prompting_PromptingServerInterceptorFactoryProtocol? {
+    return nil
+  }
+
+  internal func handle(
+    method name: Substring,
+    context: CallHandlerContext
+  ) -> GRPCServerHandlerProtocol? {
+    switch name {
+    case "Host":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Prompting_HostRequest>(),
+        responseSerializer: ProtobufSerializer<Prompting_HostResponse>(),
+        interceptors: self.interceptors?.makeHostInterceptors() ?? [],
+        wrapping: { try await self.host(requestStream: $0, responseStream: $1, context: $2) }
+      )
+
+    case "Prompt":
+      return GRPCAsyncServerHandler(
+        context: context,
+        requestDeserializer: ProtobufDeserializer<Prompting_PromptRequest>(),
+        responseSerializer: ProtobufSerializer<Prompting_PromptResponse>(),
+        interceptors: self.interceptors?.makePromptInterceptors() ?? [],
+        wrapping: { try await self.prompt(request: $0, context: $1) }
+      )
+
+    default:
+      return nil
+    }
+  }
+}
+
+internal protocol Prompting_PromptingServerInterceptorFactoryProtocol: Sendable {
+
+  /// - Returns: Interceptors to use when handling 'host'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makeHostInterceptors() -> [ServerInterceptor<Prompting_HostRequest, Prompting_HostResponse>]
+
+  /// - Returns: Interceptors to use when handling 'prompt'.
+  ///   Defaults to calling `self.makeInterceptors()`.
+  func makePromptInterceptors() -> [ServerInterceptor<Prompting_PromptRequest, Prompting_PromptResponse>]
+}
+
+internal enum Prompting_PromptingServerMetadata {
+  internal static let serviceDescriptor = GRPCServiceDescriptor(
+    name: "Prompting",
+    fullName: "prompting.Prompting",
+    methods: [
+      Prompting_PromptingServerMetadata.Methods.host,
+      Prompting_PromptingServerMetadata.Methods.prompt,
+    ]
+  )
+
+  internal enum Methods {
+    internal static let host = GRPCMethodDescriptor(
+      name: "Host",
+      path: "/prompting.Prompting/Host",
+      type: GRPCCallType.bidirectionalStreaming
+    )
+
+    internal static let prompt = GRPCMethodDescriptor(
+      name: "Prompt",
+      path: "/prompting.Prompting/Prompt",
+      type: GRPCCallType.unary
+    )
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.pb.swift
new file mode 100644
index 00000000..74afe922
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.pb.swift
@@ -0,0 +1,279 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+// swiftlint:disable all
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: service_prompting_prompting.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+//
+// This file was taken from
+// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/service/prompting/prompting.proto
+//
+// MIT License
+// 
+// Copyright (c) 2016-present Docker, Inc.
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+/// HostRequest encodes either an initial request to perform prompt hosting or a
+/// follow-up response to a message or prompt.
+struct Prompting_HostRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// AllowPrompts indicates whether or not the hoster will allow prompts. If
+  /// not, it will only receive message requests. This field may only be set on
+  /// the initial request.
+  var allowPrompts: Bool = false
+
+  /// Response is the prompt response, if any. On the initial request, this
+  /// must be an empty string. When responding to a prompt, it may be any
+  /// value. When responding to a message, it must be an empty string.
+  var response: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+/// HostResponse encodes either an initial response to perform prompt hosting or
+/// a follow-up request for messaging or prompting.
+struct Prompting_HostResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Identifier is the prompter identifier. It is only set in the initial
+  /// response sent after the initial request.
+  var identifier: String = String()
+
+  /// IsPrompt indicates if the response is requesting a prompt (as opposed to
+  /// simple message display).
+  var isPrompt: Bool = false
+
+  /// Message is the message associated with the prompt or message.
+  var message: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+/// PromptRequest encodes a request for prompting by a specific prompter.
+struct Prompting_PromptRequest: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Prompter is the prompter identifier.
+  var prompter: String = String()
+
+  /// Prompt is the prompt to present.
+  var prompt: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+/// PromptResponse encodes the response from a prompter.
+struct Prompting_PromptResponse: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// Response is the response returned by the prompter.
+  var response: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "prompting"
+
+extension Prompting_HostRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".HostRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "allowPrompts"),
+    2: .same(proto: "response"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularBoolField(value: &self.allowPrompts) }()
+      case 2: try { try decoder.decodeSingularStringField(value: &self.response) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if self.allowPrompts != false {
+      try visitor.visitSingularBoolField(value: self.allowPrompts, fieldNumber: 1)
+    }
+    if !self.response.isEmpty {
+      try visitor.visitSingularStringField(value: self.response, fieldNumber: 2)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Prompting_HostRequest, rhs: Prompting_HostRequest) -> Bool {
+    if lhs.allowPrompts != rhs.allowPrompts {return false}
+    if lhs.response != rhs.response {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Prompting_HostResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".HostResponse"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "identifier"),
+    2: .same(proto: "isPrompt"),
+    3: .same(proto: "message"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.identifier) }()
+      case 2: try { try decoder.decodeSingularBoolField(value: &self.isPrompt) }()
+      case 3: try { try decoder.decodeSingularStringField(value: &self.message) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if !self.identifier.isEmpty {
+      try visitor.visitSingularStringField(value: self.identifier, fieldNumber: 1)
+    }
+    if self.isPrompt != false {
+      try visitor.visitSingularBoolField(value: self.isPrompt, fieldNumber: 2)
+    }
+    if !self.message.isEmpty {
+      try visitor.visitSingularStringField(value: self.message, fieldNumber: 3)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Prompting_HostResponse, rhs: Prompting_HostResponse) -> Bool {
+    if lhs.identifier != rhs.identifier {return false}
+    if lhs.isPrompt != rhs.isPrompt {return false}
+    if lhs.message != rhs.message {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Prompting_PromptRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".PromptRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "prompter"),
+    2: .same(proto: "prompt"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.prompter) }()
+      case 2: try { try decoder.decodeSingularStringField(value: &self.prompt) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if !self.prompter.isEmpty {
+      try visitor.visitSingularStringField(value: self.prompter, fieldNumber: 1)
+    }
+    if !self.prompt.isEmpty {
+      try visitor.visitSingularStringField(value: self.prompt, fieldNumber: 2)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Prompting_PromptRequest, rhs: Prompting_PromptRequest) -> Bool {
+    if lhs.prompter != rhs.prompter {return false}
+    if lhs.prompt != rhs.prompt {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Prompting_PromptResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".PromptResponse"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "response"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.response) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if !self.response.isEmpty {
+      try visitor.visitSingularStringField(value: self.response, fieldNumber: 1)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Prompting_PromptResponse, rhs: Prompting_PromptResponse) -> Bool {
+    if lhs.response != rhs.response {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.proto
new file mode 100644
index 00000000..337a1544
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.proto
@@ -0,0 +1,80 @@
+/*
+ * This file was taken from
+ * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/service/prompting/prompting.proto
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2016-present Docker, Inc.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+syntax = "proto3";
+
+package prompting;
+
+option go_package = "github.com/mutagen-io/mutagen/pkg/service/prompting";
+
+// HostRequest encodes either an initial request to perform prompt hosting or a
+// follow-up response to a message or prompt.
+message HostRequest {
+    // AllowPrompts indicates whether or not the hoster will allow prompts. If
+    // not, it will only receive message requests. This field may only be set on
+    // the initial request.
+    bool allowPrompts = 1;
+    // Response is the prompt response, if any. On the initial request, this
+    // must be an empty string. When responding to a prompt, it may be any
+    // value. When responding to a message, it must be an empty string.
+    string response = 2;
+}
+
+// HostResponse encodes either an initial response to perform prompt hosting or
+// a follow-up request for messaging or prompting.
+message HostResponse {
+    // Identifier is the prompter identifier. It is only set in the initial
+    // response sent after the initial request.
+    string identifier = 1;
+    // IsPrompt indicates if the response is requesting a prompt (as opposed to
+    // simple message display).
+    bool isPrompt = 2;
+    // Message is the message associated with the prompt or message.
+    string message = 3;
+}
+
+// PromptRequest encodes a request for prompting by a specific prompter.
+message PromptRequest {
+    // Prompter is the prompter identifier.
+    string prompter = 1;
+    // Prompt is the prompt to present.
+    string prompt = 2;
+}
+
+// PromptResponse encodes the response from a prompter.
+message PromptResponse {
+    // Response is the response returned by the prompter.
+    string response = 1;
+}
+
+// Prompting allows clients to host and request prompting.
+service Prompting {
+    // Host allows clients to perform prompt hosting.
+    rpc Host(stream HostRequest) returns (stream HostResponse) {}
+    // Prompt performs prompting using a specific prompter.
+    rpc Prompt(PromptRequest) returns (PromptResponse) {}
+}
diff --git a/scripts/mutagen-proto.sh b/scripts/mutagen-proto.sh
index 4fc6cf67..fb01413b 100755
--- a/scripts/mutagen-proto.sh
+++ b/scripts/mutagen-proto.sh
@@ -4,9 +4,9 @@
 # It is very similar to `Update-Proto.ps1` on `coder/coder-desktop-windows`.
 # It's very unlikely that we'll use this script regularly.
 #
-# Unlike the Go compiler, the Swift compiler does not support multiple files 
-# with the same name in different directories. 
-# To handle this, this script flattens the directory structure of the proto 
+# Unlike the Go compiler, the Swift compiler does not support multiple files
+# with the same name in different directories.
+# To handle this, this script flattens the directory structure of the proto
 # files into the filename, i.e. `service/synchronization/synchronization.proto`
 # becomes `service_synchronization_synchronization.proto`.
 # It also updates the proto imports to use these paths.
@@ -24,7 +24,7 @@ mutagen_tag="$1"
 repo="mutagen-io/mutagen"
 proto_prefix="pkg"
 # Right now, we only care about the synchronization and daemon management gRPC
-entry_files=("service/synchronization/synchronization.proto" "service/daemon/daemon.proto")
+entry_files=("service/synchronization/synchronization.proto" "service/daemon/daemon.proto" "service/prompting/prompting.proto")
 
 out_folder="Coder-Desktop/VPNLib/FileSync/MutagenSDK"
 
@@ -33,7 +33,7 @@ if [ -d "$clone_dir" ]; then
     echo "Found existing mutagen repo at $clone_dir, checking out $mutagen_tag..."
     pushd "$clone_dir" > /dev/null
     git clean -fdx
-    
+
     current_tag=$(git name-rev --name-only HEAD)
     if [ "$current_tag" != "tags/$mutagen_tag" ]; then
         git fetch --all
@@ -62,27 +62,27 @@ add_file() {
     local proto_path="${filepath#"$clone_dir"/"$proto_prefix"/}"
     local flat_name
     flat_name=$(echo "$proto_path" | sed 's/\//_/g')
-    
+
     # Skip if already processed
     if [[ -n "${file_map[$proto_path]:-}" ]]; then
         return
     fi
-    
+
     echo "Adding $proto_path -> $flat_name"
     file_map[$proto_path]=$flat_name
     file_paths+=("$filepath")
-    
+
     # Process imports
     while IFS= read -r line; do
         if [[ $line =~ ^import\ \"(.+)\" ]]; then
             import_path="${BASH_REMATCH[1]}"
-            
+
             # Ignore google imports, as they're not vendored
             if [[ $import_path =~ ^google/ ]]; then
                 echo "Skipping $import_path"
                 continue
             fi
-            
+
             import_file_path="$clone_dir/$proto_prefix/$import_path"
             if [ -f "$import_file_path" ]; then
                 add_file "$import_file_path"
@@ -109,24 +109,24 @@ for file_path in "${file_paths[@]}"; do
     proto_path="${file_path#"$clone_dir"/"$proto_prefix"/}"
     flat_name="${file_map[$proto_path]}"
     dst_path="$out_folder/$flat_name"
-    
+
     cp -f "$file_path" "$dst_path"
-    
+
     file_header="/*\n * This file was taken from\n * https://github.com/$repo/tree/$mutagen_tag/$proto_prefix/$proto_path\n *\n$license_header\n */\n\n"
     content=$(cat "$dst_path")
     echo -e "$file_header$content" > "$dst_path"
-    
+
     tmp_file=$(mktemp)
     while IFS= read -r line; do
         if [[ $line =~ ^import\ \"(.+)\" ]]; then
             import_path="${BASH_REMATCH[1]}"
-            
+
             # Retain google imports
             if [[ $import_path =~ ^google/ ]]; then
                 echo "$line" >> "$tmp_file"
                 continue
             fi
-            
+
             # Convert import path to flattened format
             flat_import=$(echo "$import_path" | sed 's/\//_/g')
             echo "import \"$flat_import\";" >> "$tmp_file"
@@ -135,7 +135,7 @@ for file_path in "${file_paths[@]}"; do
         fi
     done < "$dst_path"
     mv "$tmp_file" "$dst_path"
-    
+
     echo "Processed $proto_path -> $flat_name"
 done
 

From 6463de007ae4363646fe39436fc40a2efe930084 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Fri, 28 Mar 2025 13:56:21 +1100
Subject: [PATCH 19/25] chore: create & delete sync sessions over gRPC (#119)

Closes #63.
---
 .../Preview Content/PreviewFileSync.swift     |   4 +
 .../Views/FileSync/FileSyncConfig.swift       |  18 ++-
 .../Views/FileSync/FileSyncSessionModal.swift |   3 +-
 Coder-Desktop/Coder-DesktopTests/Util.swift   |   4 +
 .../VPNLib/FileSync/FileSyncDaemon.swift      |  56 ++------
 .../VPNLib/FileSync/FileSyncManagement.swift  | 120 ++++++++++++++++++
 6 files changed, 155 insertions(+), 50 deletions(-)
 create mode 100644 Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift

diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift
index 8db30e3c..082c144f 100644
--- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
+++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
@@ -21,4 +21,8 @@ final class PreviewFileSync: FileSyncDaemon {
     func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
 
     func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
+
+    func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
+
+    func resumeSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 }
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
index dc83c17a..5a7257b0 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
@@ -51,11 +51,15 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
                                 loading = true
                                 defer { loading = false }
                                 do throws(DaemonError) {
+                                    // TODO: Support selecting & deleting multiple sessions at once
                                     try await fileSync.deleteSessions(ids: [selection!])
+                                    if fileSync.sessionState.isEmpty {
+                                        // Last session was deleted, stop the daemon
+                                        await fileSync.stop()
+                                    }
                                 } catch {
                                     deleteError = error
                                 }
-                                await fileSync.refreshSessions()
                                 selection = nil
                             }
                         } label: {
@@ -65,7 +69,17 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
                             if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) {
                                 Divider()
                                 Button {
-                                    // TODO: Pause & Unpause
+                                    Task {
+                                        // TODO: Support pausing & resuming multiple sessions at once
+                                        loading = true
+                                        defer { loading = false }
+                                        switch selectedSession.status {
+                                        case .paused:
+                                            try await fileSync.resumeSessions(ids: [selectedSession.id])
+                                        default:
+                                            try await fileSync.pauseSessions(ids: [selectedSession.id])
+                                        }
+                                    }
                                 } label: {
                                     switch selectedSession.status {
                                     case .paused:
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
index c0c7a35b..d3981723 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
@@ -68,7 +68,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
         }.disabled(loading)
             .alert("Error", isPresented: Binding(
                 get: { createError != nil },
-                set: { if $0 { createError = nil } }
+                set: { if !$0 { createError = nil } }
             )) {} message: {
                 Text(createError?.description ?? "An unknown error occurred.")
             }
@@ -83,7 +83,6 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
         defer { loading = false }
         do throws(DaemonError) {
             if let existingSession {
-                // TODO: Support selecting & deleting multiple sessions at once
                 try await fileSync.deleteSessions(ids: [existingSession.id])
             }
             try await fileSync.createSession(
diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
index e38fe330..cad7eaca 100644
--- a/Coder-Desktop/Coder-DesktopTests/Util.swift
+++ b/Coder-Desktop/Coder-DesktopTests/Util.swift
@@ -48,6 +48,10 @@ class MockFileSyncDaemon: FileSyncDaemon {
     }
 
     func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
+
+    func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
+
+    func resumeSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 }
 
 extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
index eafd4dc7..2adce4b2 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
@@ -15,6 +15,8 @@ public protocol FileSyncDaemon: ObservableObject {
     func refreshSessions() async
     func createSession(localPath: String, agentHost: String, remotePath: String) async throws(DaemonError)
     func deleteSessions(ids: [String]) async throws(DaemonError)
+    func pauseSessions(ids: [String]) async throws(DaemonError)
+    func resumeSessions(ids: [String]) async throws(DaemonError)
 }
 
 @MainActor
@@ -41,6 +43,9 @@ public class MutagenDaemon: FileSyncDaemon {
     private let mutagenDataDirectory: URL
     private let mutagenDaemonSocket: URL
 
+    // Managing sync sessions could take a while, especially with prompting
+    let sessionMgmtReqTimeout: TimeAmount = .seconds(15)
+
     // Non-nil when the daemon is running
     var client: DaemonClient?
     private var group: MultiThreadedEventLoopGroup?
@@ -75,6 +80,10 @@ public class MutagenDaemon: FileSyncDaemon {
                 return
             }
             await refreshSessions()
+            if sessionState.isEmpty {
+                logger.info("No sync sessions found on startup, stopping daemon")
+                await stop()
+            }
         }
     }
 
@@ -162,7 +171,7 @@ public class MutagenDaemon: FileSyncDaemon {
             // Already connected
             return
         }
-        group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+        group = MultiThreadedEventLoopGroup(numberOfThreads: 2)
         do {
             channel = try GRPCChannelPool.with(
                 target: .unixDomainSocket(mutagenDaemonSocket.path),
@@ -252,51 +261,6 @@ public class MutagenDaemon: FileSyncDaemon {
             logger.info("\(line, privacy: .public)")
         }
     }
-
-    public func refreshSessions() async {
-        guard case .running = state else { return }
-        // TODO: Implement
-    }
-
-    public func createSession(
-        localPath _: String,
-        agentHost _: String,
-        remotePath _: String
-    ) async throws(DaemonError) {
-        if case .stopped = state {
-            do throws(DaemonError) {
-                try await start()
-            } catch {
-                state = .failed(error)
-                throw error
-            }
-        }
-        // TODO: Add session
-    }
-
-    public func deleteSessions(ids _: [String]) async throws(DaemonError) {
-        // TODO: Delete session
-        await stopIfNoSessions()
-    }
-
-    private func stopIfNoSessions() async {
-        let sessions: Synchronization_ListResponse
-        do {
-            sessions = try await client!.sync.list(Synchronization_ListRequest.with { req in
-                req.selection = .with { selection in
-                    selection.all = true
-                }
-            })
-        } catch {
-            state = .failed(.daemonStartFailure(error))
-            return
-        }
-        // If there's no configured sessions, the daemon doesn't need to be running
-        if sessions.sessionStates.isEmpty {
-            logger.info("No sync sessions found")
-            await stop()
-        }
-    }
 }
 
 struct DaemonClient {
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
new file mode 100644
index 00000000..c826fa76
--- /dev/null
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
@@ -0,0 +1,120 @@
+import NIOCore
+
+public extension MutagenDaemon {
+    func refreshSessions() async {
+        guard case .running = state else { return }
+        let sessions: Synchronization_ListResponse
+        do {
+            sessions = try await client!.sync.list(Synchronization_ListRequest.with { req in
+                req.selection = .with { selection in
+                    selection.all = true
+                }
+            })
+        } catch {
+            state = .failed(.grpcFailure(error))
+            return
+        }
+        sessionState = sessions.sessionStates.map { FileSyncSession(state: $0) }
+    }
+
+    func createSession(
+        localPath: String,
+        agentHost: String,
+        remotePath: String
+    ) async throws(DaemonError) {
+        if case .stopped = state {
+            do throws(DaemonError) {
+                try await start()
+            } catch {
+                state = .failed(error)
+                throw error
+            }
+        }
+        let (stream, promptID) = try await host()
+        defer { stream.cancel() }
+        let req = Synchronization_CreateRequest.with { req in
+            req.prompter = promptID
+            req.specification = .with { spec in
+                spec.alpha = .with { alpha in
+                    alpha.protocol = .local
+                    alpha.path = localPath
+                }
+                spec.beta = .with { beta in
+                    beta.protocol = .ssh
+                    beta.host = agentHost
+                    beta.path = remotePath
+                }
+                // TODO: Ingest a config from somewhere
+                spec.configuration = Synchronization_Configuration()
+                spec.configurationAlpha = Synchronization_Configuration()
+                spec.configurationBeta = Synchronization_Configuration()
+            }
+        }
+        do {
+            // The first creation will need to transfer the agent binary
+            // TODO: Because this is pretty long, we should show progress updates
+            // using the prompter messages
+            _ = try await client!.sync.create(req, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout * 4)))
+        } catch {
+            throw .grpcFailure(error)
+        }
+        await refreshSessions()
+    }
+
+    func deleteSessions(ids: [String]) async throws(DaemonError) {
+        // Terminating sessions does not require prompting, according to the
+        // Mutagen CLI
+        let (stream, promptID) = try await host(allowPrompts: false)
+        defer { stream.cancel() }
+        guard case .running = state else { return }
+        do {
+            _ = try await client!.sync.terminate(Synchronization_TerminateRequest.with { req in
+                req.prompter = promptID
+                req.selection = .with { selection in
+                    selection.specifications = ids
+                }
+            }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout)))
+        } catch {
+            throw .grpcFailure(error)
+        }
+        await refreshSessions()
+    }
+
+    func pauseSessions(ids: [String]) async throws(DaemonError) {
+        // Pausing sessions does not require prompting, according to the
+        // Mutagen CLI
+        let (stream, promptID) = try await host(allowPrompts: false)
+        defer { stream.cancel() }
+        guard case .running = state else { return }
+        do {
+            _ = try await client!.sync.pause(Synchronization_PauseRequest.with { req in
+                req.prompter = promptID
+                req.selection = .with { selection in
+                    selection.specifications = ids
+                }
+            }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout)))
+        } catch {
+            throw .grpcFailure(error)
+        }
+        await refreshSessions()
+    }
+
+    func resumeSessions(ids: [String]) async throws(DaemonError) {
+        // Resuming sessions does not require prompting, according to the
+        // Mutagen CLI
+        let (stream, promptID) = try await host(allowPrompts: false)
+        defer { stream.cancel() }
+        guard case .running = state else { return }
+        do {
+            _ = try await client!.sync.resume(Synchronization_ResumeRequest.with { req in
+                req.prompter = promptID
+                req.selection = .with { selection in
+                    selection.specifications = ids
+                }
+            }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout)))
+        } catch {
+            throw .grpcFailure(error)
+        }
+        await refreshSessions()
+    }
+}

From ff033e1c1be54ebba1fb9257bfc215759b86cb30 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 2 Apr 2025 14:15:23 +1100
Subject: [PATCH 20/25] feat: add file sync daemon error handling to the UI
 (#122)

If file sync is working, but a session has errored, an icon will be displayed on the main menu.
e.g. for:
<img width="512" alt="image" src="https://github.com/user-attachments/assets/aac73c99-e318-44d3-9091-3f5d99239037" />
This icon & tooltip are displayed:
<img width="256" alt="image" src="https://github.com/user-attachments/assets/f4b7ba15-dca2-4819-aaa8-07e74e4e238d" />

If file sync is not working altogether, due to the daemon crashing, the same icon will be displayed with a different tooltip on hover:
<img width="254" alt="image" src="https://github.com/user-attachments/assets/efc87c1d-acac-4353-a3c9-c04908762d28" />

Once the config menu is opened, an alert is displayed, and the daemon log file is opened.
<img width="1354" alt="image" src="https://github.com/user-attachments/assets/98b44f6e-4584-4ad3-a237-1557ec5edab1" />

From there, the Daemon can be restarted, or the alert can be dismissed without restarting. The latter provides users an out if the daemon were to crash on launch repeatedly.
---
 .../Preview Content/PreviewFileSync.swift     |   4 +-
 .../Views/FileSync/FileSyncConfig.swift       | 191 +++++++++++-------
 .../Coder-Desktop/Views/VPN/VPNMenu.swift     |   9 +-
 Coder-Desktop/Coder-DesktopTests/Util.swift   |   6 +-
 .../VPNLib/FileSync/FileSyncDaemon.swift      |  68 +++++--
 5 files changed, 189 insertions(+), 89 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift
index 082c144f..608b3684 100644
--- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
+++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
@@ -2,6 +2,8 @@ import VPNLib
 
 @MainActor
 final class PreviewFileSync: FileSyncDaemon {
+    var logFile: URL = .init(filePath: "~/log.txt")!
+
     var sessionState: [VPNLib.FileSyncSession] = []
 
     var state: DaemonState = .running
@@ -10,7 +12,7 @@ final class PreviewFileSync: FileSyncDaemon {
 
     func refreshSessions() async {}
 
-    func start() async throws(DaemonError) {
+    func tryStart() async {
         state = .running
     }
 
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
index 5a7257b0..ff4fbe1a 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
@@ -11,6 +11,8 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
 
     @State private var loading: Bool = false
     @State private var deleteError: DaemonError?
+    @State private var isVisible: Bool = false
+    @State private var dontRetry: Bool = false
 
     var body: some View {
         Group {
@@ -36,87 +38,140 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
             .frame(minWidth: 400, minHeight: 200)
             .padding(.bottom, 25)
             .overlay(alignment: .bottom) {
-                VStack(alignment: .leading, spacing: 0) {
-                    Divider()
-                    HStack(spacing: 0) {
-                        Button {
-                            addingNewSession = true
-                        } label: {
-                            Image(systemName: "plus")
-                                .frame(width: 24, height: 24)
-                        }.disabled(vpn.menuState.agents.isEmpty)
+                tableFooter
+            }
+            // Only the table & footer should be disabled if the daemon has crashed
+            // otherwise the alert buttons will be disabled too
+        }.disabled(fileSync.state.isFailed)
+            .sheet(isPresented: $addingNewSession) {
+                FileSyncSessionModal<VPN, FS>()
+                    .frame(width: 700)
+            }.sheet(item: $editingSession) { session in
+                FileSyncSessionModal<VPN, FS>(existingSession: session)
+                    .frame(width: 700)
+            }.alert("Error", isPresented: Binding(
+                get: { deleteError != nil },
+                set: { isPresented in
+                    if !isPresented {
+                        deleteError = nil
+                    }
+                }
+            )) {} message: {
+                Text(deleteError?.description ?? "An unknown error occurred.")
+            }.alert("Error", isPresented: Binding(
+                // We only show the alert if the file config window is open
+                // Users will see the alert symbol on the menu bar to prompt them to
+                // open it. The requirement on `!loading` prevents the alert from
+                // re-opening immediately.
+                get: { !loading && isVisible && fileSync.state.isFailed },
+                set: { isPresented in
+                    if !isPresented {
+                        if dontRetry {
+                            dontRetry = false
+                            return
+                        }
+                        loading = true
+                        Task {
+                            await fileSync.tryStart()
+                            loading = false
+                        }
+                    }
+                }
+            )) {
+                Button("Retry") {}
+                // This gives the user an out if the daemon is crashing on launch,
+                // they can cancel the alert, and it will reappear if they re-open the
+                // file sync window.
+                Button("Cancel", role: .cancel) {
+                    dontRetry = true
+                }
+            } message: {
+                Text("""
+                File sync daemon failed. The daemon log file at\n\(fileSync.logFile.path)\nhas been opened.
+                """).onAppear {
+                    // Open the log file in the default editor
+                    NSWorkspace.shared.open(fileSync.logFile)
+                }
+            }.task {
+                // When the Window is visible, poll for session updates every
+                // two seconds.
+                while !Task.isCancelled {
+                    if !fileSync.state.isFailed {
+                        await fileSync.refreshSessions()
+                    }
+                    try? await Task.sleep(for: .seconds(2))
+                }
+            }.onAppear {
+                isVisible = true
+            }.onDisappear {
+                isVisible = false
+                // If the failure alert is dismissed without restarting the daemon,
+                // (by clicking cancel) this makes it clear that the daemon
+                // is still in a failed state.
+            }.navigationTitle("Coder File Sync \(fileSync.state.isFailed ? "- Failed" : "")")
+            .disabled(loading)
+    }
+
+    var tableFooter: some View {
+        VStack(alignment: .leading, spacing: 0) {
+            Divider()
+            HStack(spacing: 0) {
+                Button {
+                    addingNewSession = true
+                } label: {
+                    Image(systemName: "plus")
+                        .frame(width: 24, height: 24)
+                }.disabled(vpn.menuState.agents.isEmpty)
+                Divider()
+                Button {
+                    Task {
+                        loading = true
+                        defer { loading = false }
+                        do throws(DaemonError) {
+                            // TODO: Support selecting & deleting multiple sessions at once
+                            try await fileSync.deleteSessions(ids: [selection!])
+                            if fileSync.sessionState.isEmpty {
+                                // Last session was deleted, stop the daemon
+                                await fileSync.stop()
+                            }
+                        } catch {
+                            deleteError = error
+                        }
+                        selection = nil
+                    }
+                } label: {
+                    Image(systemName: "minus").frame(width: 24, height: 24)
+                }.disabled(selection == nil)
+                if let selection {
+                    if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) {
                         Divider()
                         Button {
                             Task {
+                                // TODO: Support pausing & resuming multiple sessions at once
                                 loading = true
                                 defer { loading = false }
-                                do throws(DaemonError) {
-                                    // TODO: Support selecting & deleting multiple sessions at once
-                                    try await fileSync.deleteSessions(ids: [selection!])
-                                    if fileSync.sessionState.isEmpty {
-                                        // Last session was deleted, stop the daemon
-                                        await fileSync.stop()
-                                    }
-                                } catch {
-                                    deleteError = error
+                                switch selectedSession.status {
+                                case .paused:
+                                    try await fileSync.resumeSessions(ids: [selectedSession.id])
+                                default:
+                                    try await fileSync.pauseSessions(ids: [selectedSession.id])
                                 }
-                                selection = nil
                             }
                         } label: {
-                            Image(systemName: "minus").frame(width: 24, height: 24)
-                        }.disabled(selection == nil)
-                        if let selection {
-                            if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) {
-                                Divider()
-                                Button {
-                                    Task {
-                                        // TODO: Support pausing & resuming multiple sessions at once
-                                        loading = true
-                                        defer { loading = false }
-                                        switch selectedSession.status {
-                                        case .paused:
-                                            try await fileSync.resumeSessions(ids: [selectedSession.id])
-                                        default:
-                                            try await fileSync.pauseSessions(ids: [selectedSession.id])
-                                        }
-                                    }
-                                } label: {
-                                    switch selectedSession.status {
-                                    case .paused:
-                                        Image(systemName: "play").frame(width: 24, height: 24)
-                                    default:
-                                        Image(systemName: "pause").frame(width: 24, height: 24)
-                                    }
-                                }
+                            switch selectedSession.status {
+                            case .paused:
+                                Image(systemName: "play").frame(width: 24, height: 24)
+                            default:
+                                Image(systemName: "pause").frame(width: 24, height: 24)
                             }
                         }
                     }
-                    .buttonStyle(.borderless)
                 }
-                .background(.primary.opacity(0.04))
-                .fixedSize(horizontal: false, vertical: true)
-            }
-        }.sheet(isPresented: $addingNewSession) {
-            FileSyncSessionModal<VPN, FS>()
-                .frame(width: 700)
-        }.sheet(item: $editingSession) { session in
-            FileSyncSessionModal<VPN, FS>(existingSession: session)
-                .frame(width: 700)
-        }.alert("Error", isPresented: Binding(
-            get: { deleteError != nil },
-            set: { isPresented in
-                if !isPresented {
-                    deleteError = nil
-                }
-            }
-        )) {} message: {
-            Text(deleteError?.description ?? "An unknown error occurred.")
-        }.task {
-            while !Task.isCancelled {
-                await fileSync.refreshSessions()
-                try? await Task.sleep(for: .seconds(2))
             }
-        }.disabled(loading)
+            .buttonStyle(.borderless)
+        }
+        .background(.primary.opacity(0.04))
+        .fixedSize(horizontal: false, vertical: true)
     }
 }
 
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
index b3fa74e2..207f0d96 100644
--- a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
@@ -68,11 +68,12 @@ struct VPNMenu<VPN: VPNService, FS: FileSyncDaemon>: View {
                     } label: {
                         ButtonRowView {
                             HStack {
-                                // TODO: A future PR will provide users a way to recover from a daemon failure without
-                                // needing to restart the app
-                                if case .failed = fileSync.state, sessionsHaveError(fileSync.sessionState) {
+                                if fileSync.state.isFailed || sessionsHaveError(fileSync.sessionState) {
                                     Image(systemName: "exclamationmark.arrow.trianglehead.2.clockwise.rotate.90")
-                                        .frame(width: 12, height: 12).help("One or more sync sessions have errors")
+                                        .frame(width: 12, height: 12)
+                                        .help(fileSync.state.isFailed ?
+                                            "The file sync daemon encountered an error" :
+                                            "One or more file sync sessions have errors")
                                 }
                                 Text("File sync")
                             }
diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
index cad7eaca..bfae5167 100644
--- a/Coder-Desktop/Coder-DesktopTests/Util.swift
+++ b/Coder-Desktop/Coder-DesktopTests/Util.swift
@@ -29,6 +29,8 @@ class MockVPNService: VPNService, ObservableObject {
 
 @MainActor
 class MockFileSyncDaemon: FileSyncDaemon {
+    var logFile: URL = .init(filePath: "~/log.txt")
+
     var sessionState: [VPNLib.FileSyncSession] = []
 
     func refreshSessions() async {}
@@ -37,9 +39,7 @@ class MockFileSyncDaemon: FileSyncDaemon {
 
     var state: VPNLib.DaemonState = .running
 
-    func start() async throws(VPNLib.DaemonError) {
-        return
-    }
+    func tryStart() async {}
 
     func stop() async {}
 
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
index 2adce4b2..1bac93cb 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
@@ -10,7 +10,8 @@ import SwiftUI
 public protocol FileSyncDaemon: ObservableObject {
     var state: DaemonState { get }
     var sessionState: [FileSyncSession] { get }
-    func start() async throws(DaemonError)
+    var logFile: URL { get }
+    func tryStart() async
     func stop() async
     func refreshSessions() async
     func createSession(localPath: String, agentHost: String, remotePath: String) async throws(DaemonError)
@@ -43,6 +44,8 @@ public class MutagenDaemon: FileSyncDaemon {
     private let mutagenDataDirectory: URL
     private let mutagenDaemonSocket: URL
 
+    public let logFile: URL
+
     // Managing sync sessions could take a while, especially with prompting
     let sessionMgmtReqTimeout: TimeAmount = .seconds(15)
 
@@ -50,6 +53,7 @@ public class MutagenDaemon: FileSyncDaemon {
     var client: DaemonClient?
     private var group: MultiThreadedEventLoopGroup?
     private var channel: GRPCChannel?
+    private var waitForExit: (@Sendable () async -> Void)?
 
     // Protect start & stop transitions against re-entrancy
     private let transition = AsyncSemaphore(value: 1)
@@ -63,6 +67,7 @@ public class MutagenDaemon: FileSyncDaemon {
         self.mutagenPath = mutagenPath
         self.mutagenDataDirectory = mutagenDataDirectory
         mutagenDaemonSocket = mutagenDataDirectory.appending(path: "daemon").appending(path: "daemon.sock")
+        logFile = mutagenDataDirectory.appending(path: "daemon.log")
         // It shouldn't be fatal if the app was built without Mutagen embedded,
         // but file sync will be unavailable.
         if mutagenPath == nil {
@@ -87,33 +92,41 @@ public class MutagenDaemon: FileSyncDaemon {
         }
     }
 
-    public func start() async throws(DaemonError) {
+    public func tryStart() async {
+        if case .failed = state { state = .stopped }
+        do throws(DaemonError) {
+            try await start()
+        } catch {
+            state = .failed(error)
+        }
+    }
+
+    func start() async throws(DaemonError) {
         if case .unavailable = state { return }
 
         // Stop an orphaned daemon, if there is one
         try? await connect()
         await stop()
 
+        // Creating the same process twice from Swift will crash the MainActor,
+        // so we need to wait for an earlier process to die
+        await waitForExit?()
+
         await transition.wait()
         defer { transition.signal() }
         logger.info("starting mutagen daemon")
 
         mutagenProcess = createMutagenProcess()
-        // swiftlint:disable:next large_tuple
-        let (standardOutput, standardError, waitForExit): (Pipe.AsyncBytes, Pipe.AsyncBytes, @Sendable () async -> Void)
+        let (standardError, waitForExit): (Pipe.AsyncBytes, @Sendable () async -> Void)
         do {
-            (standardOutput, standardError, waitForExit) = try mutagenProcess!.run()
+            (_, standardError, waitForExit) = try mutagenProcess!.run()
         } catch {
             throw .daemonStartFailure(error)
         }
+        self.waitForExit = waitForExit
 
         Task {
-            await streamHandler(io: standardOutput)
-            logger.info("standard output stream closed")
-        }
-
-        Task {
-            await streamHandler(io: standardError)
+            await handleDaemonLogs(io: standardError)
             logger.info("standard error stream closed")
         }
 
@@ -256,10 +269,30 @@ public class MutagenDaemon: FileSyncDaemon {
         }
     }
 
-    private func streamHandler(io: Pipe.AsyncBytes) async {
+    private func handleDaemonLogs(io: Pipe.AsyncBytes) async {
+        if !FileManager.default.fileExists(atPath: logFile.path) {
+            guard FileManager.default.createFile(atPath: logFile.path, contents: nil) else {
+                logger.error("Failed to create log file")
+                return
+            }
+        }
+
+        guard let fileHandle = try? FileHandle(forWritingTo: logFile) else {
+            logger.error("Failed to open log file for writing")
+            return
+        }
+
         for await line in io.lines {
             logger.info("\(line, privacy: .public)")
+
+            do {
+                try fileHandle.write(contentsOf: Data("\(line)\n".utf8))
+            } catch {
+                logger.error("Failed to write to daemon log file: \(error)")
+            }
         }
+
+        try? fileHandle.close()
     }
 }
 
@@ -282,7 +315,7 @@ public enum DaemonState {
         case .stopped:
             "Stopped"
         case let .failed(error):
-            "Failed: \(error)"
+            "\(error.description)"
         case .unavailable:
             "Unavailable"
         }
@@ -300,6 +333,15 @@ public enum DaemonState {
             .gray
         }
     }
+
+    // `if case`s are a pain to work with: they're not bools (such as for ORing)
+    // and you can't negate them without doing `if case .. {} else`.
+    public var isFailed: Bool {
+        if case .failed = self {
+            return true
+        }
+        return false
+    }
 }
 
 public enum DaemonError: Error {

From 1fd58555a541cd32f2ddf99e9195e7c235ef5945 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 2 Apr 2025 14:18:26 +1100
Subject: [PATCH 21/25] feat: support restarting file sync sessions (#124)

<img width="210" alt="image" src="https://github.com/user-attachments/assets/215ec4f1-c2a9-4e64-af8f-4aab0f2fa352" />

Equivalent to `mutagen sync restart`, such as for resolving safety checks: https://mutagen.io/documentation/introduction/getting-started/#resetting-sessions

Also adds an error alert for all fallible operations on the config window.
---
 .../Preview Content/PreviewFileSync.swift     |   2 +
 .../Views/FileSync/FileSyncConfig.swift       | 128 +++++++++++-------
 Coder-Desktop/Coder-DesktopTests/Util.swift   |   2 +
 .../VPNLib/FileSync/FileSyncDaemon.swift      |   1 +
 .../VPNLib/FileSync/FileSyncManagement.swift  |  23 +++-
 5 files changed, 105 insertions(+), 51 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift
index 608b3684..45597166 100644
--- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
+++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
@@ -27,4 +27,6 @@ final class PreviewFileSync: FileSyncDaemon {
     func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 
     func resumeSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
+
+    func resetSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 }
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
index ff4fbe1a..6b147add 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
@@ -10,7 +10,7 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
     @State private var editingSession: FileSyncSession?
 
     @State private var loading: Bool = false
-    @State private var deleteError: DaemonError?
+    @State private var actionError: DaemonError?
     @State private var isVisible: Bool = false
     @State private var dontRetry: Bool = false
 
@@ -50,14 +50,14 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
                 FileSyncSessionModal<VPN, FS>(existingSession: session)
                     .frame(width: 700)
             }.alert("Error", isPresented: Binding(
-                get: { deleteError != nil },
+                get: { actionError != nil },
                 set: { isPresented in
                     if !isPresented {
-                        deleteError = nil
+                        actionError = nil
                     }
                 }
             )) {} message: {
-                Text(deleteError?.description ?? "An unknown error occurred.")
+                Text(actionError?.description ?? "An unknown error occurred.")
             }.alert("Error", isPresented: Binding(
                 // We only show the alert if the file config window is open
                 // Users will see the alert symbol on the menu bar to prompt them to
@@ -89,7 +89,7 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
                 Text("""
                 File sync daemon failed. The daemon log file at\n\(fileSync.logFile.path)\nhas been opened.
                 """).onAppear {
-                    // Open the log file in the default editor
+                    // Opens the log file in Console
                     NSWorkspace.shared.open(fileSync.logFile)
                 }
             }.task {
@@ -120,58 +120,90 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
                     addingNewSession = true
                 } label: {
                     Image(systemName: "plus")
-                        .frame(width: 24, height: 24)
+                        .frame(width: 24, height: 24).help("Create")
                 }.disabled(vpn.menuState.agents.isEmpty)
-                Divider()
-                Button {
-                    Task {
-                        loading = true
-                        defer { loading = false }
-                        do throws(DaemonError) {
-                            // TODO: Support selecting & deleting multiple sessions at once
-                            try await fileSync.deleteSessions(ids: [selection!])
-                            if fileSync.sessionState.isEmpty {
-                                // Last session was deleted, stop the daemon
-                                await fileSync.stop()
-                            }
-                        } catch {
-                            deleteError = error
+                sessionControls
+            }
+            .buttonStyle(.borderless)
+        }
+        .background(.primary.opacity(0.04))
+        .fixedSize(horizontal: false, vertical: true)
+    }
+
+    var sessionControls: some View {
+        Group {
+            if let selection {
+                if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) {
+                    Divider()
+                    Button { Task { await delete(session: selectedSession) } }
+                        label: {
+                            Image(systemName: "minus").frame(width: 24, height: 24).help("Terminate")
                         }
-                        selection = nil
-                    }
-                } label: {
-                    Image(systemName: "minus").frame(width: 24, height: 24)
-                }.disabled(selection == nil)
-                if let selection {
-                    if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) {
-                        Divider()
-                        Button {
-                            Task {
-                                // TODO: Support pausing & resuming multiple sessions at once
-                                loading = true
-                                defer { loading = false }
-                                switch selectedSession.status {
-                                case .paused:
-                                    try await fileSync.resumeSessions(ids: [selectedSession.id])
-                                default:
-                                    try await fileSync.pauseSessions(ids: [selectedSession.id])
-                                }
-                            }
-                        } label: {
+                    Divider()
+                    Button { Task { await pauseResume(session: selectedSession) } }
+                        label: {
                             switch selectedSession.status {
-                            case .paused:
-                                Image(systemName: "play").frame(width: 24, height: 24)
+                            case .paused, .error(.haltedOnRootEmptied),
+                                 .error(.haltedOnRootDeletion),
+                                 .error(.haltedOnRootTypeChange):
+                                Image(systemName: "play").frame(width: 24, height: 24).help("Pause")
                             default:
-                                Image(systemName: "pause").frame(width: 24, height: 24)
+                                Image(systemName: "pause").frame(width: 24, height: 24).help("Resume")
                             }
                         }
-                    }
+                    Divider()
+                    Button { Task { await reset(session: selectedSession) } }
+                        label: {
+                            Image(systemName: "arrow.clockwise").frame(width: 24, height: 24).help("Reset")
+                        }
                 }
             }
-            .buttonStyle(.borderless)
         }
-        .background(.primary.opacity(0.04))
-        .fixedSize(horizontal: false, vertical: true)
+    }
+
+    // TODO: Support selecting & deleting multiple sessions at once
+    func delete(session _: FileSyncSession) async {
+        loading = true
+        defer { loading = false }
+        do throws(DaemonError) {
+            try await fileSync.deleteSessions(ids: [selection!])
+            if fileSync.sessionState.isEmpty {
+                // Last session was deleted, stop the daemon
+                await fileSync.stop()
+            }
+        } catch {
+            actionError = error
+        }
+        selection = nil
+    }
+
+    // TODO: Support pausing & resuming multiple sessions at once
+    func pauseResume(session: FileSyncSession) async {
+        loading = true
+        defer { loading = false }
+        do throws(DaemonError) {
+            switch session.status {
+            case .paused, .error(.haltedOnRootEmptied),
+                 .error(.haltedOnRootDeletion),
+                 .error(.haltedOnRootTypeChange):
+                try await fileSync.resumeSessions(ids: [session.id])
+            default:
+                try await fileSync.pauseSessions(ids: [session.id])
+            }
+        } catch {
+            actionError = error
+        }
+    }
+
+    // TODO: Support restarting multiple sessions at once
+    func reset(session: FileSyncSession) async {
+        loading = true
+        defer { loading = false }
+        do throws(DaemonError) {
+            try await fileSync.resetSessions(ids: [session.id])
+        } catch {
+            actionError = error
+        }
     }
 }
 
diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
index bfae5167..4301cbc4 100644
--- a/Coder-Desktop/Coder-DesktopTests/Util.swift
+++ b/Coder-Desktop/Coder-DesktopTests/Util.swift
@@ -52,6 +52,8 @@ class MockFileSyncDaemon: FileSyncDaemon {
     func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 
     func resumeSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
+
+    func resetSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 }
 
 extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
index 1bac93cb..9e10f2ac 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
@@ -18,6 +18,7 @@ public protocol FileSyncDaemon: ObservableObject {
     func deleteSessions(ids: [String]) async throws(DaemonError)
     func pauseSessions(ids: [String]) async throws(DaemonError)
     func resumeSessions(ids: [String]) async throws(DaemonError)
+    func resetSessions(ids: [String]) async throws(DaemonError)
 }
 
 @MainActor
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
index c826fa76..d1d3f6ca 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
@@ -100,9 +100,8 @@ public extension MutagenDaemon {
     }
 
     func resumeSessions(ids: [String]) async throws(DaemonError) {
-        // Resuming sessions does not require prompting, according to the
-        // Mutagen CLI
-        let (stream, promptID) = try await host(allowPrompts: false)
+        // Resuming sessions does use prompting, as it may start a new SSH connection
+        let (stream, promptID) = try await host(allowPrompts: true)
         defer { stream.cancel() }
         guard case .running = state else { return }
         do {
@@ -117,4 +116,22 @@ public extension MutagenDaemon {
         }
         await refreshSessions()
     }
+
+    func resetSessions(ids: [String]) async throws(DaemonError) {
+        // Resetting a session involves pausing & resuming, so it does use prompting
+        let (stream, promptID) = try await host(allowPrompts: true)
+        defer { stream.cancel() }
+        guard case .running = state else { return }
+        do {
+            _ = try await client!.sync.reset(Synchronization_ResetRequest.with { req in
+                req.prompter = promptID
+                req.selection = .with { selection in
+                    selection.specifications = ids
+                }
+            }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout)))
+        } catch {
+            throw .grpcFailure(error)
+        }
+        await refreshSessions()
+    }
 }

From fe208010e19f69985d5a901d192212820e73ef6e Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 2 Apr 2025 17:52:09 +1100
Subject: [PATCH 22/25] feat: add conflict descriptions and file sync context
 menu (#126)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Last QoL PR for now..

This adds buttons to the alt click context menu:
<img width="971" alt="Screenshot 2025-03-28 at 3 25 12 pm" src="https://github.com/user-attachments/assets/2477ee7d-2466-4fa5-9f7f-53a711ffdd64" />



And it adds a brief description of each conflict to the status tooltip:
<img width="405" alt="image" src="https://github.com/user-attachments/assets/e513faf1-414f-4612-902d-204b277a34b1" />

There's three cases for now. The first is just a basic file conflict, the second is if there's a type conflict (file, directory, symlink, etc), and the third is self-explanatory.

We'll need to come up with a proper design for how we show conflicts, so this implementation is just to not leave users in the dark if they run into any.
---
 .../Views/FileSync/FileSyncConfig.swift       |  37 +++--
 .../VPNLib/FileSync/FileSyncSession.swift     |  32 +++-
 .../VPNLib/FileSync/MutagenConvert.swift      | 140 +++++++++++++++++-
 3 files changed, 183 insertions(+), 26 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
index 6b147add..345928b6 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
@@ -29,12 +29,23 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
                 TableColumn("Size") { Text($0.localSize.humanSizeBytes).help($0.sizeDescription) }
                     .width(min: 60, ideal: 80)
             }
-            .contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { _ in },
-                         primaryAction: { selectedSessions in
-                             if let session = selectedSessions.first {
-                                 editingSession = fileSync.sessionState.first(where: { $0.id == session })
-                             }
-                         })
+            .contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { selections in
+                // TODO: We only support single selections for now
+                if let selected = selections.first,
+                   let session = fileSync.sessionState.first(where: { $0.id == selected })
+                {
+                    Button("Edit") { editingSession = session }
+                    Button(session.status.isResumable ? "Resume" : "Pause")
+                        { Task { await pauseResume(session: session) } }
+                    Button("Reset") { Task { await reset(session: session) } }
+                    Button("Terminate") { Task { await delete(session: session) } }
+                }
+            },
+            primaryAction: { selectedSessions in
+                if let session = selectedSessions.first {
+                    editingSession = fileSync.sessionState.first(where: { $0.id == session })
+                }
+            })
             .frame(minWidth: 400, minHeight: 200)
             .padding(.bottom, 25)
             .overlay(alignment: .bottom) {
@@ -142,12 +153,9 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
                     Divider()
                     Button { Task { await pauseResume(session: selectedSession) } }
                         label: {
-                            switch selectedSession.status {
-                            case .paused, .error(.haltedOnRootEmptied),
-                                 .error(.haltedOnRootDeletion),
-                                 .error(.haltedOnRootTypeChange):
+                            if selectedSession.status.isResumable {
                                 Image(systemName: "play").frame(width: 24, height: 24).help("Pause")
-                            default:
+                            } else {
                                 Image(systemName: "pause").frame(width: 24, height: 24).help("Resume")
                             }
                         }
@@ -182,12 +190,9 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
         loading = true
         defer { loading = false }
         do throws(DaemonError) {
-            switch session.status {
-            case .paused, .error(.haltedOnRootEmptied),
-                 .error(.haltedOnRootDeletion),
-                 .error(.haltedOnRootTypeChange):
+            if session.status.isResumable {
                 try await fileSync.resumeSessions(ids: [session.id])
-            default:
+            } else {
                 try await fileSync.pauseSessions(ids: [session.id])
             }
         } catch {
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift
index d586908d..b0c43f32 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift
@@ -46,7 +46,12 @@ public struct FileSyncSession: Identifiable {
         }
         if case .error = status {} else {
             if state.conflicts.count > 0 {
-                status = .conflicts
+                status = .conflicts(
+                    formatConflicts(
+                        conflicts: state.conflicts,
+                        excludedConflicts: state.excludedConflicts
+                    )
+                )
             }
         }
         self.status = status
@@ -121,7 +126,7 @@ public enum FileSyncStatus {
     case error(FileSyncErrorStatus)
     case ok
     case paused
-    case conflicts
+    case conflicts(String)
     case working(FileSyncWorkingStatus)
 
     public var color: Color {
@@ -168,8 +173,8 @@ public enum FileSyncStatus {
             "The session is watching for filesystem changes."
         case .paused:
             "The session is paused."
-        case .conflicts:
-            "The session has conflicts that need to be resolved."
+        case let .conflicts(details):
+            "The session has conflicts that need to be resolved:\n\n\(details)"
         case let .working(status):
             status.description
         }
@@ -178,6 +183,18 @@ public enum FileSyncStatus {
     public var column: some View {
         Text(type).foregroundColor(color)
     }
+
+    public var isResumable: Bool {
+        switch self {
+        case .paused,
+             .error(.haltedOnRootEmptied),
+             .error(.haltedOnRootDeletion),
+             .error(.haltedOnRootTypeChange):
+            true
+        default:
+            false
+        }
+    }
 }
 
 public enum FileSyncWorkingStatus {
@@ -272,8 +289,8 @@ public enum FileSyncErrorStatus {
 }
 
 public enum FileSyncEndpoint {
-    case local
-    case remote
+    case alpha
+    case beta
 }
 
 public enum FileSyncProblemType {
@@ -284,6 +301,7 @@ public enum FileSyncProblemType {
 public enum FileSyncError {
     case generic(String)
     case problem(FileSyncEndpoint, FileSyncProblemType, path: String, error: String)
+    case excludedProblems(FileSyncEndpoint, FileSyncProblemType, UInt64)
 
     var description: String {
         switch self {
@@ -291,6 +309,8 @@ public enum FileSyncError {
             error
         case let .problem(endpoint, type, path, error):
             "\(endpoint) \(type) error at \(path): \(error)"
+        case let .excludedProblems(endpoint, type, count):
+            "+ \(count) \(endpoint) \(type) problems"
         }
     }
 }
diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift
index 8a59b238..b422d86a 100644
--- a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift
+++ b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift
@@ -40,16 +40,28 @@ func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] {
         errors.append(.generic(state.lastError))
     }
     for problem in state.alphaState.scanProblems {
-        errors.append(.problem(.local, .scan, path: problem.path, error: problem.error))
+        errors.append(.problem(.alpha, .scan, path: problem.path, error: problem.error))
     }
     for problem in state.alphaState.transitionProblems {
-        errors.append(.problem(.local, .transition, path: problem.path, error: problem.error))
+        errors.append(.problem(.alpha, .transition, path: problem.path, error: problem.error))
     }
     for problem in state.betaState.scanProblems {
-        errors.append(.problem(.remote, .scan, path: problem.path, error: problem.error))
+        errors.append(.problem(.beta, .scan, path: problem.path, error: problem.error))
     }
     for problem in state.betaState.transitionProblems {
-        errors.append(.problem(.remote, .transition, path: problem.path, error: problem.error))
+        errors.append(.problem(.beta, .transition, path: problem.path, error: problem.error))
+    }
+    if state.alphaState.excludedScanProblems > 0 {
+        errors.append(.excludedProblems(.alpha, .scan, state.alphaState.excludedScanProblems))
+    }
+    if state.alphaState.excludedTransitionProblems > 0 {
+        errors.append(.excludedProblems(.alpha, .transition, state.alphaState.excludedTransitionProblems))
+    }
+    if state.betaState.excludedScanProblems > 0 {
+        errors.append(.excludedProblems(.beta, .scan, state.betaState.excludedScanProblems))
+    }
+    if state.betaState.excludedTransitionProblems > 0 {
+        errors.append(.excludedProblems(.beta, .transition, state.betaState.excludedTransitionProblems))
     }
     return errors
 }
@@ -80,3 +92,123 @@ extension Prompting_HostResponse {
         }
     }
 }
+
+// Translated from `cmd/mutagen/sync/list_monitor_common.go`
+func formatConflicts(conflicts: [Core_Conflict], excludedConflicts: UInt64) -> String {
+    var result = ""
+    for (i, conflict) in conflicts.enumerated() {
+        var changesByPath: [String: (alpha: [Core_Change], beta: [Core_Change])] = [:]
+
+        // Group alpha changes by path
+        for alphaChange in conflict.alphaChanges {
+            let path = alphaChange.path
+            if changesByPath[path] == nil {
+                changesByPath[path] = (alpha: [], beta: [])
+            }
+            changesByPath[path]!.alpha.append(alphaChange)
+        }
+
+        // Group beta changes by path
+        for betaChange in conflict.betaChanges {
+            let path = betaChange.path
+            if changesByPath[path] == nil {
+                changesByPath[path] = (alpha: [], beta: [])
+            }
+            changesByPath[path]!.beta.append(betaChange)
+        }
+
+        result += formatChanges(changesByPath)
+
+        if i < conflicts.count - 1 || excludedConflicts > 0 {
+            result += "\n"
+        }
+    }
+
+    if excludedConflicts > 0 {
+        result += "...+\(excludedConflicts) more conflicts...\n"
+    }
+
+    return result
+}
+
+func formatChanges(_ changesByPath: [String: (alpha: [Core_Change], beta: [Core_Change])]) -> String {
+    var result = ""
+
+    for (path, changes) in changesByPath {
+        if changes.alpha.count == 1, changes.beta.count == 1 {
+            // Simple message for basic file conflicts
+            if changes.alpha[0].hasNew,
+               changes.beta[0].hasNew,
+               changes.alpha[0].new.kind == .file,
+               changes.beta[0].new.kind == .file
+            {
+                result += "File: '\(formatPath(path))'\n"
+                continue
+            }
+            // Friendly message for `<non-existent -> !<non-existent>` conflicts
+            if !changes.alpha[0].hasOld,
+               !changes.beta[0].hasOld,
+               changes.alpha[0].hasNew,
+               changes.beta[0].hasNew
+            {
+                result += """
+                An entry, '\(formatPath(path))', was created on both endpoints that does not match.
+                You can resolve this conflict by deleting one of the entries.\n
+                """
+                continue
+            }
+        }
+
+        let formattedPath = formatPath(path)
+        result += "Path: '\(formattedPath)'\n"
+
+        // TODO: Local & Remote should be replaced with Alpha & Beta, once it's possible to configure which is which
+
+        if !changes.alpha.isEmpty {
+            result += " Local changes:\n"
+            for change in changes.alpha {
+                let old = formatEntry(change.hasOld ? change.old : nil)
+                let new = formatEntry(change.hasNew ? change.new : nil)
+                result += "  \(old) → \(new)\n"
+            }
+        }
+
+        if !changes.beta.isEmpty {
+            result += " Remote changes:\n"
+            for change in changes.beta {
+                let old = formatEntry(change.hasOld ? change.old : nil)
+                let new = formatEntry(change.hasNew ? change.new : nil)
+                result += "  \(old) → \(new)\n"
+            }
+        }
+    }
+
+    return result
+}
+
+func formatPath(_ path: String) -> String {
+    path.isEmpty ? "<root>" : path
+}
+
+func formatEntry(_ entry: Core_Entry?) -> String {
+    guard let entry else {
+        return "<non-existent>"
+    }
+
+    switch entry.kind {
+    case .directory:
+        return "Directory"
+    case .file:
+        return entry.executable ? "Executable File" : "File"
+    case .symbolicLink:
+        return "Symbolic Link (\(entry.target))"
+    case .untracked:
+        return "Untracked content"
+    case .problematic:
+        return "Problematic content (\(entry.problem))"
+    case .UNRECOGNIZED:
+        return "<unknown>"
+    case .phantomDirectory:
+        return "Phantom Directory"
+    }
+}

From 9f625fd6d2c40a1e3350ed0f9d7f864d00b6aa26 Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Mon, 7 Apr 2025 16:52:09 +1000
Subject: [PATCH 23/25] fix: improve file sync agent picker (#128)

Previously, the agent/workspace picker when creating a file sync session had the user choose between instances of the `Agent` struct. This meant the value would get unselected were the status of the agent to change. I'm not sure why I had the picker select the entire struct instead of just the hostname.
---
 .../Views/FileSync/FileSyncConfig.swift         |  9 ---------
 .../Views/FileSync/FileSyncSessionModal.swift   | 17 +++++++++--------
 .../Coder-Desktop/Views/VPN/VPNMenu.swift       |  6 ++++++
 3 files changed, 15 insertions(+), 17 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
index 345928b6..dc946c83 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
@@ -103,15 +103,6 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
                     // Opens the log file in Console
                     NSWorkspace.shared.open(fileSync.logFile)
                 }
-            }.task {
-                // When the Window is visible, poll for session updates every
-                // two seconds.
-                while !Task.isCancelled {
-                    if !fileSync.state.isFailed {
-                        await fileSync.refreshSessions()
-                    }
-                    try? await Task.sleep(for: .seconds(2))
-                }
             }.onAppear {
                 isVisible = true
             }.onDisappear {
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
index d3981723..0e42ea0c 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
@@ -8,7 +8,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
     @EnvironmentObject private var fileSync: FS
 
     @State private var localPath: String = ""
-    @State private var workspace: Agent?
+    @State private var remoteHostname: String?
     @State private var remotePath: String = ""
 
     @State private var loading: Bool = false
@@ -37,12 +37,12 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
                     }
                 }
                 Section {
-                    Picker("Workspace", selection: $workspace) {
+                    Picker("Workspace", selection: $remoteHostname) {
                         ForEach(agents, id: \.id) { agent in
-                            Text(agent.primaryHost!).tag(agent)
+                            Text(agent.primaryHost!).tag(agent.primaryHost!)
                         }
                         // HACK: Silence error logs for no-selection.
-                        Divider().tag(nil as Agent?)
+                        Divider().tag(nil as String?)
                     }
                 }
                 Section {
@@ -55,15 +55,16 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
                 Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
                 Button(existingSession == nil ? "Add" : "Save") { Task { await submit() }}
                     .keyboardShortcut(.defaultAction)
+                    .disabled(localPath.isEmpty || remotePath.isEmpty || remoteHostname == nil)
             }.padding(20)
         }.onAppear {
             if let existingSession {
                 localPath = existingSession.alphaPath
-                workspace = agents.first { $0.primaryHost == existingSession.agentHost }
+                remoteHostname = agents.first { $0.primaryHost == existingSession.agentHost }?.primaryHost
                 remotePath = existingSession.betaPath
             } else {
                 // Set the picker to the first agent by default
-                workspace = agents.first
+                remoteHostname = agents.first?.primaryHost
             }
         }.disabled(loading)
             .alert("Error", isPresented: Binding(
@@ -76,7 +77,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
 
     func submit() async {
         createError = nil
-        guard let workspace else {
+        guard let remoteHostname else {
             return
         }
         loading = true
@@ -87,7 +88,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
             }
             try await fileSync.createSession(
                 localPath: localPath,
-                agentHost: workspace.primaryHost!,
+                agentHost: remoteHostname,
                 remotePath: remotePath
             )
         } catch {
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
index 207f0d96..83757efd 100644
--- a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
@@ -116,6 +116,12 @@ struct VPNMenu<VPN: VPNService, FS: FileSyncDaemon>: View {
             .environmentObject(vpn)
             .environmentObject(state)
             .onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
+            .task {
+                while !Task.isCancelled {
+                    await fileSync.refreshSessions()
+                    try? await Task.sleep(for: .seconds(2))
+                }
+            }
     }
 
     private var vpnDisabled: Bool {

From de604d752d0d39b819234da6f98667b37e5fc14e Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 9 Apr 2025 20:01:25 +1000
Subject: [PATCH 24/25] feat: add remote folder picker to file sync GUI (#127)

Closes #65.


https://github.com/user-attachments/assets/f5f9ae14-7bfe-4520-8b05-a1ff8ad0ada0


https://github.com/user-attachments/assets/34706ed8-15db-409a-9a69-972fab75a3ae

<img width="295" alt="image" src="https://github.com/user-attachments/assets/94e875bf-b70f-458d-aaa4-27aa1023607e" />
---
 Coder-Desktop/Coder-Desktop/Info.plist        |   9 +
 .../Views/FileSync/FilePicker.swift           | 232 ++++++++++++++++++
 .../Views/FileSync/FileSyncSessionModal.swift |  15 +-
 .../Coder-DesktopTests/FilePickerTests.swift  | 115 +++++++++
 Coder-Desktop/Coder-DesktopTests/Util.swift   |  25 ++
 Coder-Desktop/CoderSDK/AgentClient.swift      |   7 +
 Coder-Desktop/CoderSDK/AgentLS.swift          |  43 ++++
 Coder-Desktop/CoderSDK/Client.swift           |   2 +-
 8 files changed, 446 insertions(+), 2 deletions(-)
 create mode 100644 Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift
 create mode 100644 Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
 create mode 100644 Coder-Desktop/CoderSDK/AgentClient.swift
 create mode 100644 Coder-Desktop/CoderSDK/AgentLS.swift

diff --git a/Coder-Desktop/Coder-Desktop/Info.plist b/Coder-Desktop/Coder-Desktop/Info.plist
index 8609906b..5e59b253 100644
--- a/Coder-Desktop/Coder-Desktop/Info.plist
+++ b/Coder-Desktop/Coder-Desktop/Info.plist
@@ -2,6 +2,15 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>NSAppTransportSecurity</key>
+	<dict>
+		<!--
+		Required to make HTTP (not HTTPS) requests to workspace agents
+		(i.e. workspace.coder:4). These are already encrypted over wireguard.
+		-->
+		<key>NSAllowsArbitraryLoads</key>
+		<true/>
+	</dict>
 	<key>NetworkExtension</key>
 	<dict>
 		<key>NEMachServiceName</key>
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift
new file mode 100644
index 00000000..4ee31a62
--- /dev/null
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift
@@ -0,0 +1,232 @@
+import CoderSDK
+import Foundation
+import SwiftUI
+
+struct FilePicker: View {
+    @Environment(\.dismiss) var dismiss
+    @StateObject private var model: FilePickerModel
+    @State private var selection: FilePickerEntryModel?
+
+    @Binding var outputAbsPath: String
+
+    let inspection = Inspection<Self>()
+
+    init(
+        host: String,
+        outputAbsPath: Binding<String>
+    ) {
+        _model = StateObject(wrappedValue: FilePickerModel(host: host))
+        _outputAbsPath = outputAbsPath
+    }
+
+    var body: some View {
+        VStack(spacing: 0) {
+            if model.rootIsLoading {
+                Spacer()
+                ProgressView()
+                    .controlSize(.large)
+                Spacer()
+            } else if let loadError = model.error {
+                Text("\(loadError.description)")
+                    .font(.headline)
+                    .foregroundColor(.red)
+                    .multilineTextAlignment(.center)
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    .padding()
+            } else {
+                List(selection: $selection) {
+                    ForEach(model.rootEntries) { entry in
+                        FilePickerEntry(entry: entry).tag(entry)
+                    }
+                }.contextMenu(
+                    forSelectionType: FilePickerEntryModel.self,
+                    menu: { _ in },
+                    primaryAction: { selections in
+                        // Per the type of `selection`, this will only ever be a set of
+                        // one entry.
+                        selections.forEach { entry in withAnimation { entry.isExpanded.toggle() } }
+                    }
+                ).listStyle(.sidebar)
+            }
+            Divider()
+            HStack {
+                Spacer()
+                Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
+                Button("Select", action: submit).keyboardShortcut(.defaultAction).disabled(selection == nil)
+            }.padding(20)
+        }
+        .onAppear {
+            model.loadRoot()
+        }
+        .onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
+    }
+
+    private func submit() {
+        guard let selection else { return }
+        outputAbsPath = selection.absolute_path
+        dismiss()
+    }
+}
+
+@MainActor
+class FilePickerModel: ObservableObject {
+    @Published var rootEntries: [FilePickerEntryModel] = []
+    @Published var rootIsLoading: Bool = false
+    @Published var error: ClientError?
+
+    // It's important that `AgentClient` is a reference type (class)
+    // as we were having performance issues with a struct (unless it was a binding).
+    let client: AgentClient
+
+    init(host: String) {
+        client = AgentClient(agentHost: host)
+    }
+
+    func loadRoot() {
+        error = nil
+        rootIsLoading = true
+        Task {
+            defer { rootIsLoading = false }
+            do throws(ClientError) {
+                rootEntries = try await client
+                    .listAgentDirectory(.init(path: [], relativity: .root))
+                    .toModels(client: client)
+            } catch {
+                self.error = error
+            }
+        }
+    }
+}
+
+struct FilePickerEntry: View {
+    @ObservedObject var entry: FilePickerEntryModel
+
+    var body: some View {
+        Group {
+            if entry.dir {
+                directory
+            } else {
+                Label(entry.name, systemImage: "doc")
+                    .help(entry.absolute_path)
+                    .selectionDisabled()
+                    .foregroundColor(.secondary)
+            }
+        }
+    }
+
+    private var directory: some View {
+        DisclosureGroup(isExpanded: $entry.isExpanded) {
+            if let entries = entry.entries {
+                ForEach(entries) { entry in
+                    FilePickerEntry(entry: entry).tag(entry)
+                }
+            }
+        } label: {
+            Label {
+                Text(entry.name)
+                ZStack {
+                    ProgressView().controlSize(.small).opacity(entry.isLoading && entry.error == nil ? 1 : 0)
+                    Image(systemName: "exclamationmark.triangle.fill")
+                        .opacity(entry.error != nil ? 1 : 0)
+                }
+            } icon: {
+                Image(systemName: "folder")
+            }.help(entry.error != nil ? entry.error!.description : entry.absolute_path)
+        }
+    }
+}
+
+@MainActor
+class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
+    nonisolated let id: [String]
+    let name: String
+    // Components of the path as an array
+    let path: [String]
+    let absolute_path: String
+    let dir: Bool
+
+    let client: AgentClient
+
+    @Published var entries: [FilePickerEntryModel]?
+    @Published var isLoading = false
+    @Published var error: ClientError?
+    @Published private var innerIsExpanded = false
+    var isExpanded: Bool {
+        get { innerIsExpanded }
+        set {
+            if !newValue {
+                withAnimation { self.innerIsExpanded = false }
+            } else {
+                Task {
+                    self.loadEntries()
+                }
+            }
+        }
+    }
+
+    init(
+        name: String,
+        client: AgentClient,
+        absolute_path: String,
+        path: [String],
+        dir: Bool = false,
+        entries: [FilePickerEntryModel]? = nil
+    ) {
+        self.name = name
+        self.client = client
+        self.path = path
+        self.dir = dir
+        self.absolute_path = absolute_path
+        self.entries = entries
+
+        // Swift Arrays are copy on write
+        id = path
+    }
+
+    func loadEntries() {
+        self.error = nil
+        withAnimation { isLoading = true }
+        Task {
+            defer {
+                withAnimation {
+                    isLoading = false
+                    innerIsExpanded = true
+                }
+            }
+            do throws(ClientError) {
+                entries = try await client
+                    .listAgentDirectory(.init(path: path, relativity: .root))
+                    .toModels(client: client)
+            } catch {
+                self.error = error
+            }
+        }
+    }
+
+    nonisolated static func == (lhs: FilePickerEntryModel, rhs: FilePickerEntryModel) -> Bool {
+        lhs.id == rhs.id
+    }
+
+    nonisolated func hash(into hasher: inout Hasher) {
+        hasher.combine(id)
+    }
+}
+
+extension LSResponse {
+    @MainActor
+    func toModels(client: AgentClient) -> [FilePickerEntryModel] {
+        contents.compactMap { entry in
+            // Filter dotfiles from the picker
+            guard !entry.name.hasPrefix(".") else { return nil }
+
+            return FilePickerEntryModel(
+                name: entry.name,
+                client: client,
+                absolute_path: entry.absolute_path_string,
+                path: self.absolute_path + [entry.name],
+                dir: entry.is_dir,
+                entries: nil
+            )
+        }
+    }
+}
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
index 0e42ea0c..7b902f21 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
@@ -13,6 +13,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
 
     @State private var loading: Bool = false
     @State private var createError: DaemonError?
+    @State private var pickingRemote: Bool = false
 
     var body: some View {
         let agents = vpn.menuState.onlineAgents
@@ -46,7 +47,16 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
                     }
                 }
                 Section {
-                    TextField("Remote Path", text: $remotePath)
+                    HStack(spacing: 5) {
+                        TextField("Remote Path", text: $remotePath)
+                        Spacer()
+                        Button {
+                            pickingRemote = true
+                        } label: {
+                            Image(systemName: "folder")
+                        }.disabled(remoteHostname == nil)
+                            .help(remoteHostname == nil ? "Select a workspace first" : "Open File Picker")
+                    }
                 }
             }.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
             Divider()
@@ -72,6 +82,9 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
                 set: { if !$0 { createError = nil } }
             )) {} message: {
                 Text(createError?.description ?? "An unknown error occurred.")
+            }.sheet(isPresented: $pickingRemote) {
+                FilePicker(host: remoteHostname!, outputAbsPath: $remotePath)
+                    .frame(width: 300, height: 400)
             }
     }
 
diff --git a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
new file mode 100644
index 00000000..61bf2196
--- /dev/null
+++ b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
@@ -0,0 +1,115 @@
+@testable import Coder_Desktop
+@testable import CoderSDK
+import Mocker
+import SwiftUI
+import Testing
+import ViewInspector
+
+@MainActor
+@Suite(.timeLimit(.minutes(1)))
+struct FilePickerTests {
+    let mockResponse: LSResponse
+
+    init() {
+        mockResponse = LSResponse(
+            absolute_path: ["/"],
+            absolute_path_string: "/",
+            contents: [
+                LSFile(name: "home", absolute_path_string: "/home", is_dir: true),
+                LSFile(name: "tmp", absolute_path_string: "/tmp", is_dir: true),
+                LSFile(name: "etc", absolute_path_string: "/etc", is_dir: true),
+                LSFile(name: "README.md", absolute_path_string: "/README.md", is_dir: false),
+            ]
+        )
+    }
+
+    @Test
+    func testLoadError() async throws {
+        let host = "test-error.coder"
+        let sut = FilePicker(host: host, outputAbsPath: .constant(""))
+        let view = sut
+
+        let url = URL(string: "http://\(host):4")!
+
+        let errorMessage = "Connection failed"
+        Mock(
+            url: url.appendingPathComponent("/api/v0/list-directory"),
+            contentType: .json,
+            statusCode: 500,
+            data: [.post: errorMessage.data(using: .utf8)!]
+        ).register()
+
+        try await ViewHosting.host(view) {
+            try await sut.inspection.inspect { view in
+                try #expect(await eventually { @MainActor in
+                    let text = try view.find(ViewType.Text.self)
+                    return try text.string().contains("Connection failed")
+                })
+            }
+        }
+    }
+
+    @Test
+    func testSuccessfulFileLoad() async throws {
+        let host = "test-success.coder"
+        let sut = FilePicker(host: host, outputAbsPath: .constant(""))
+        let view = sut
+
+        let url = URL(string: "http://\(host):4")!
+
+        try Mock(
+            url: url.appendingPathComponent("/api/v0/list-directory"),
+            statusCode: 200,
+            data: [.post: Client.encoder.encode(mockResponse)]
+        ).register()
+
+        try await ViewHosting.host(view) {
+            try await sut.inspection.inspect { view in
+                try #expect(await eventually { @MainActor in
+                    _ = try view.find(ViewType.List.self)
+                    return true
+                })
+                _ = try view.find(text: "README.md")
+                _ = try view.find(text: "home")
+                let selectButton = try view.find(button: "Select")
+                #expect(selectButton.isDisabled())
+            }
+        }
+    }
+
+    @Test
+    func testDirectoryExpansion() async throws {
+        let host = "test-expansion.coder"
+        let sut = FilePicker(host: host, outputAbsPath: .constant(""))
+        let view = sut
+
+        let url = URL(string: "http://\(host):4")!
+
+        try Mock(
+            url: url.appendingPathComponent("/api/v0/list-directory"),
+            statusCode: 200,
+            data: [.post: Client.encoder.encode(mockResponse)]
+        ).register()
+
+        try await ViewHosting.host(view) {
+            try await sut.inspection.inspect { view in
+                try #expect(await eventually { @MainActor in
+                    _ = try view.find(ViewType.List.self)
+                    return true
+                })
+
+                let disclosureGroup = try view.find(ViewType.DisclosureGroup.self)
+                #expect(view.findAll(ViewType.DisclosureGroup.self).count == 3)
+                try disclosureGroup.expand()
+
+                // Disclosure group should expand out to 3 more directories
+                try #expect(await eventually { @MainActor in
+                    return try view.findAll(ViewType.DisclosureGroup.self).count == 6
+                })
+            }
+        }
+    }
+
+    // TODO: The writing of more extensive tests is blocked by ViewInspector,
+    // as it can't select an item in a list...
+}
diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
index 4301cbc4..249aa10b 100644
--- a/Coder-Desktop/Coder-DesktopTests/Util.swift
+++ b/Coder-Desktop/Coder-DesktopTests/Util.swift
@@ -57,3 +57,28 @@ class MockFileSyncDaemon: FileSyncDaemon {
 }
 
 extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}
+
+public func eventually(
+    timeout: Duration = .milliseconds(500),
+    interval: Duration = .milliseconds(10),
+    condition: @escaping () async throws -> Bool
+) async throws -> Bool {
+    let endTime = ContinuousClock.now.advanced(by: timeout)
+
+    var lastError: Error?
+
+    while ContinuousClock.now < endTime {
+        do {
+            if try await condition() { return true }
+            lastError = nil
+        } catch {
+            lastError = error
+            try await Task.sleep(for: interval)
+        }
+    }
+
+    if let lastError {
+        throw lastError
+    }
+    return false
+}
diff --git a/Coder-Desktop/CoderSDK/AgentClient.swift b/Coder-Desktop/CoderSDK/AgentClient.swift
new file mode 100644
index 00000000..ecdd3d43
--- /dev/null
+++ b/Coder-Desktop/CoderSDK/AgentClient.swift
@@ -0,0 +1,7 @@
+public final class AgentClient: Sendable {
+    let client: Client
+
+    public init(agentHost: String) {
+        client = Client(url: URL(string: "http://\(agentHost):4")!)
+    }
+}
diff --git a/Coder-Desktop/CoderSDK/AgentLS.swift b/Coder-Desktop/CoderSDK/AgentLS.swift
new file mode 100644
index 00000000..7110f405
--- /dev/null
+++ b/Coder-Desktop/CoderSDK/AgentLS.swift
@@ -0,0 +1,43 @@
+public extension AgentClient {
+    func listAgentDirectory(_ req: LSRequest) async throws(ClientError) -> LSResponse {
+        let res = try await client.request("/api/v0/list-directory", method: .post, body: req)
+        guard res.resp.statusCode == 200 else {
+            throw client.responseAsError(res)
+        }
+        return try client.decode(LSResponse.self, from: res.data)
+    }
+}
+
+public struct LSRequest: Sendable, Codable {
+    // e.g. [], ["repos", "coder"]
+    public let path: [String]
+    // Whether the supplied path is relative to the user's home directory,
+    // or the root directory.
+    public let relativity: LSRelativity
+
+    public init(path: [String], relativity: LSRelativity) {
+        self.path = path
+        self.relativity = relativity
+    }
+
+    public enum LSRelativity: String, Sendable, Codable {
+        case root
+        case home
+    }
+}
+
+public struct LSResponse: Sendable, Codable {
+    public let absolute_path: [String]
+    // e.g. Windows: "C:\\Users\\coder"
+    //      Linux: "/home/coder"
+    public let absolute_path_string: String
+    public let contents: [LSFile]
+}
+
+public struct LSFile: Sendable, Codable {
+    public let name: String
+    // e.g. "C:\\Users\\coder\\hello.txt"
+    //      "/home/coder/hello.txt"
+    public let absolute_path_string: String
+    public let is_dir: Bool
+}
diff --git a/Coder-Desktop/CoderSDK/Client.swift b/Coder-Desktop/CoderSDK/Client.swift
index 239db14a..98e1c8a9 100644
--- a/Coder-Desktop/CoderSDK/Client.swift
+++ b/Coder-Desktop/CoderSDK/Client.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-public struct Client {
+public struct Client: Sendable {
     public let url: URL
     public var token: String?
     public var headers: [HTTPHeader]

From 8067574baf913919149553878349f19e4541707e Mon Sep 17 00:00:00 2001
From: Ethan <39577870+ethanndickson@users.noreply.github.com>
Date: Wed, 9 Apr 2025 20:04:23 +1000
Subject: [PATCH 25/25] chore: add file sync daemon tests (#129)

These are just regression tests for the core file sync daemon functionality.

Also has sync sessions ignore VCS directories by default, as per the file sync RFC.
---
 .../Coder-Desktop/Coder_DesktopApp.swift      |   6 +-
 .../Preview Content/PreviewFileSync.swift     |   2 +-
 .../Views/FileSync/FileSyncConfig.swift       |   4 -
 .../Views/FileSync/FileSyncSessionModal.swift |   7 +-
 .../Coder-DesktopTests/FilePickerTests.swift  |   4 +-
 .../FileSyncDaemonTests.swift                 | 167 ++++++++++++++++++
 Coder-Desktop/Coder-DesktopTests/Util.swift   |  28 +--
 .../VPNLib/FileSync/FileSyncDaemon.swift      |  23 +--
 .../VPNLib/FileSync/FileSyncManagement.swift  |  92 +++++++---
 Coder-Desktop/project.yml                     |   4 +-
 Makefile                                      |   2 +-
 11 files changed, 274 insertions(+), 65 deletions(-)
 create mode 100644 Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift

diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index a110432d..30ea7e7e 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -51,9 +51,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
         #elseif arch(x86_64)
             let mutagenBinary = "mutagen-darwin-amd64"
         #endif
-        fileSyncDaemon = MutagenDaemon(
+        let fileSyncDaemon = MutagenDaemon(
             mutagenPath: Bundle.main.url(forResource: mutagenBinary, withExtension: nil)
         )
+        Task {
+            await fileSyncDaemon.tryStart()
+        }
+        self.fileSyncDaemon = fileSyncDaemon
     }
 
     func applicationDidFinishLaunching(_: Notification) {
diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift
index 45597166..1253e427 100644
--- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
+++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
@@ -20,7 +20,7 @@ final class PreviewFileSync: FileSyncDaemon {
         state = .stopped
     }
 
-    func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
+    func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
 
     func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
index dc946c83..74006359 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
@@ -166,10 +166,6 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
         defer { loading = false }
         do throws(DaemonError) {
             try await fileSync.deleteSessions(ids: [selection!])
-            if fileSync.sessionState.isEmpty {
-                // Last session was deleted, stop the daemon
-                await fileSync.stop()
-            }
         } catch {
             actionError = error
         }
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
index 7b902f21..66b20baf 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
@@ -100,9 +100,10 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
                 try await fileSync.deleteSessions(ids: [existingSession.id])
             }
             try await fileSync.createSession(
-                localPath: localPath,
-                agentHost: remoteHostname,
-                remotePath: remotePath
+                arg: .init(
+                    alpha: .init(path: localPath, protocolKind: .local),
+                    beta: .init(path: remotePath, protocolKind: .ssh(host: remoteHostname))
+                )
             )
         } catch {
             createError = error
diff --git a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
index 61bf2196..d361581e 100644
--- a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
@@ -103,8 +103,8 @@ struct FilePickerTests {
                 try disclosureGroup.expand()
 
                 // Disclosure group should expand out to 3 more directories
-                try #expect(await eventually { @MainActor in
-                    return try view.findAll(ViewType.DisclosureGroup.self).count == 6
+                #expect(await eventually { @MainActor in
+                    return view.findAll(ViewType.DisclosureGroup.self).count == 6
                 })
             }
         }
diff --git a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift
new file mode 100644
index 00000000..916faf64
--- /dev/null
+++ b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift
@@ -0,0 +1,167 @@
+@testable import Coder_Desktop
+import Foundation
+import GRPC
+import NIO
+import Subprocess
+import Testing
+import VPNLib
+import XCTest
+
+@MainActor
+@Suite(.timeLimit(.minutes(1)))
+class FileSyncDaemonTests {
+    let tempDir: URL
+    let mutagenBinary: URL
+    let mutagenDataDirectory: URL
+    let mutagenAlphaDirectory: URL
+    let mutagenBetaDirectory: URL
+
+    // Before each test
+    init() throws {
+        tempDir = FileManager.default.makeTempDir()!
+        #if arch(arm64)
+            let binaryName = "mutagen-darwin-arm64"
+        #elseif arch(x86_64)
+            let binaryName = "mutagen-darwin-amd64"
+        #endif
+        mutagenBinary = Bundle.main.url(forResource: binaryName, withExtension: nil)!
+        mutagenDataDirectory = tempDir.appending(path: "mutagen")
+        mutagenAlphaDirectory = tempDir.appending(path: "alpha")
+        try FileManager.default.createDirectory(at: mutagenAlphaDirectory, withIntermediateDirectories: true)
+        mutagenBetaDirectory = tempDir.appending(path: "beta")
+        try FileManager.default.createDirectory(at: mutagenBetaDirectory, withIntermediateDirectories: true)
+    }
+
+    // After each test
+    deinit {
+        try? FileManager.default.removeItem(at: tempDir)
+    }
+
+    private func statesEqual(_ first: DaemonState, _ second: DaemonState) -> Bool {
+        switch (first, second) {
+        case (.stopped, .stopped):
+            true
+        case (.running, .running):
+            true
+        case (.unavailable, .unavailable):
+            true
+        default:
+            false
+        }
+    }
+
+    @Test
+    func fullSync() async throws {
+        let daemon = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
+        #expect(statesEqual(daemon.state, .stopped))
+        #expect(daemon.sessionState.count == 0)
+
+        // The daemon won't start until we create a session
+        await daemon.tryStart()
+        #expect(statesEqual(daemon.state, .stopped))
+        #expect(daemon.sessionState.count == 0)
+
+        try await daemon.createSession(
+            arg: .init(
+                alpha: .init(
+                    path: mutagenAlphaDirectory.path(),
+                    protocolKind: .local
+                ),
+                beta: .init(
+                    path: mutagenBetaDirectory.path(),
+                    protocolKind: .local
+                )
+            )
+        )
+
+        // Daemon should have started itself
+        #expect(statesEqual(daemon.state, .running))
+        #expect(daemon.sessionState.count == 1)
+
+        // Write a file to Alpha
+        let alphaFile = mutagenAlphaDirectory.appendingPathComponent("test.txt")
+        try "Hello, World!".write(to: alphaFile, atomically: true, encoding: .utf8)
+        #expect(
+            await eventually(timeout: .seconds(5), interval: .milliseconds(100)) { @MainActor in
+                return FileManager.default.fileExists(
+                    atPath: self.mutagenBetaDirectory.appending(path: "test.txt").path()
+                )
+            })
+
+        try await daemon.deleteSessions(ids: daemon.sessionState.map(\.id))
+        #expect(daemon.sessionState.count == 0)
+        // Daemon should have stopped itself once all sessions are deleted
+        #expect(statesEqual(daemon.state, .stopped))
+    }
+
+    @Test
+    func autoStopStart() async throws {
+        let daemon = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
+        #expect(statesEqual(daemon.state, .stopped))
+        #expect(daemon.sessionState.count == 0)
+
+        try await daemon.createSession(
+            arg: .init(
+                alpha: .init(
+                    path: mutagenAlphaDirectory.path(),
+                    protocolKind: .local
+                ),
+                beta: .init(
+                    path: mutagenBetaDirectory.path(),
+                    protocolKind: .local
+                )
+            )
+        )
+
+        try await daemon.createSession(
+            arg: .init(
+                alpha: .init(
+                    path: mutagenAlphaDirectory.path(),
+                    protocolKind: .local
+                ),
+                beta: .init(
+                    path: mutagenBetaDirectory.path(),
+                    protocolKind: .local
+                )
+            )
+        )
+
+        #expect(statesEqual(daemon.state, .running))
+        #expect(daemon.sessionState.count == 2)
+
+        try await daemon.deleteSessions(ids: [daemon.sessionState[0].id])
+        #expect(daemon.sessionState.count == 1)
+        #expect(statesEqual(daemon.state, .running))
+
+        try await daemon.deleteSessions(ids: [daemon.sessionState[0].id])
+        #expect(daemon.sessionState.count == 0)
+        #expect(statesEqual(daemon.state, .stopped))
+    }
+
+    @Test
+    func orphaned() async throws {
+        let daemon1 = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
+        await daemon1.refreshSessions()
+        try await daemon1.createSession(arg:
+            .init(
+                alpha: .init(
+                    path: mutagenAlphaDirectory.path(),
+                    protocolKind: .local
+                ),
+                beta: .init(
+                    path: mutagenBetaDirectory.path(),
+                    protocolKind: .local
+                )
+            )
+        )
+        #expect(statesEqual(daemon1.state, .running))
+        #expect(daemon1.sessionState.count == 1)
+
+        let daemon2 = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
+        await daemon2.tryStart()
+        #expect(statesEqual(daemon2.state, .running))
+
+        // Daemon 2 should have killed daemon 1, causing it to fail
+        #expect(daemon1.state.isFailed)
+    }
+}
diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
index 249aa10b..c5239a92 100644
--- a/Coder-Desktop/Coder-DesktopTests/Util.swift
+++ b/Coder-Desktop/Coder-DesktopTests/Util.swift
@@ -47,7 +47,7 @@ class MockFileSyncDaemon: FileSyncDaemon {
         []
     }
 
-    func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
+    func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
 
     func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 
@@ -61,24 +61,32 @@ extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}
 public func eventually(
     timeout: Duration = .milliseconds(500),
     interval: Duration = .milliseconds(10),
-    condition: @escaping () async throws -> Bool
-) async throws -> Bool {
+    condition: @Sendable () async throws -> Bool
+) async rethrows -> Bool {
     let endTime = ContinuousClock.now.advanced(by: timeout)
 
-    var lastError: Error?
-
     while ContinuousClock.now < endTime {
         do {
             if try await condition() { return true }
-            lastError = nil
         } catch {
-            lastError = error
             try await Task.sleep(for: interval)
         }
     }
 
-    if let lastError {
-        throw lastError
+    return try await condition()
+}
+
+extension FileManager {
+    func makeTempDir() -> URL? {
+        let tempDirectory = FileManager.default.temporaryDirectory
+        let directoryName = String(Int.random(in: 0 ..< 1_000_000))
+        let directoryURL = tempDirectory.appendingPathComponent(directoryName)
+
+        do {
+            try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
+            return directoryURL
+        } catch {
+            return nil
+        }
     }
-    return false
 }
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
index 9e10f2ac..7f300fbe 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
@@ -14,7 +14,7 @@ public protocol FileSyncDaemon: ObservableObject {
     func tryStart() async
     func stop() async
     func refreshSessions() async
-    func createSession(localPath: String, agentHost: String, remotePath: String) async throws(DaemonError)
+    func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError)
     func deleteSessions(ids: [String]) async throws(DaemonError)
     func pauseSessions(ids: [String]) async throws(DaemonError)
     func resumeSessions(ids: [String]) async throws(DaemonError)
@@ -76,21 +76,6 @@ public class MutagenDaemon: FileSyncDaemon {
             state = .unavailable
             return
         }
-
-        // If there are sync sessions, the daemon should be running
-        Task {
-            do throws(DaemonError) {
-                try await start()
-            } catch {
-                state = .failed(error)
-                return
-            }
-            await refreshSessions()
-            if sessionState.isEmpty {
-                logger.info("No sync sessions found on startup, stopping daemon")
-                await stop()
-            }
-        }
     }
 
     public func tryStart() async {
@@ -99,6 +84,12 @@ public class MutagenDaemon: FileSyncDaemon {
             try await start()
         } catch {
             state = .failed(error)
+            return
+        }
+        await refreshSessions()
+        if sessionState.isEmpty {
+            logger.info("No sync sessions found on startup, stopping daemon")
+            await stop()
         }
     }
 
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
index d1d3f6ca..aaf86b18 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
@@ -17,11 +17,7 @@ public extension MutagenDaemon {
         sessionState = sessions.sessionStates.map { FileSyncSession(state: $0) }
     }
 
-    func createSession(
-        localPath: String,
-        agentHost: String,
-        remotePath: String
-    ) async throws(DaemonError) {
+    func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError) {
         if case .stopped = state {
             do throws(DaemonError) {
                 try await start()
@@ -35,17 +31,14 @@ public extension MutagenDaemon {
         let req = Synchronization_CreateRequest.with { req in
             req.prompter = promptID
             req.specification = .with { spec in
-                spec.alpha = .with { alpha in
-                    alpha.protocol = .local
-                    alpha.path = localPath
+                spec.alpha = arg.alpha.mutagenURL
+                spec.beta = arg.beta.mutagenURL
+                // TODO: Ingest configs from somewhere
+                spec.configuration = .with {
+                    // ALWAYS ignore VCS directories for now
+                    // https://mutagen.io/documentation/synchronization/version-control-systems/
+                    $0.ignoreVcsmode = .ignore
                 }
-                spec.beta = .with { beta in
-                    beta.protocol = .ssh
-                    beta.host = agentHost
-                    beta.path = remotePath
-                }
-                // TODO: Ingest a config from somewhere
-                spec.configuration = Synchronization_Configuration()
                 spec.configurationAlpha = Synchronization_Configuration()
                 spec.configurationBeta = Synchronization_Configuration()
             }
@@ -64,20 +57,26 @@ public extension MutagenDaemon {
     func deleteSessions(ids: [String]) async throws(DaemonError) {
         // Terminating sessions does not require prompting, according to the
         // Mutagen CLI
-        let (stream, promptID) = try await host(allowPrompts: false)
-        defer { stream.cancel() }
-        guard case .running = state else { return }
         do {
-            _ = try await client!.sync.terminate(Synchronization_TerminateRequest.with { req in
-                req.prompter = promptID
-                req.selection = .with { selection in
-                    selection.specifications = ids
-                }
-            }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout)))
-        } catch {
-            throw .grpcFailure(error)
+            let (stream, promptID) = try await host(allowPrompts: false)
+            defer { stream.cancel() }
+            guard case .running = state else { return }
+            do {
+                _ = try await client!.sync.terminate(Synchronization_TerminateRequest.with { req in
+                    req.prompter = promptID
+                    req.selection = .with { selection in
+                        selection.specifications = ids
+                    }
+                }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout)))
+            } catch {
+                throw .grpcFailure(error)
+            }
         }
         await refreshSessions()
+        if sessionState.isEmpty {
+            // Last session was deleted, stop the daemon
+            await stop()
+        }
     }
 
     func pauseSessions(ids: [String]) async throws(DaemonError) {
@@ -135,3 +134,44 @@ public extension MutagenDaemon {
         await refreshSessions()
     }
 }
+
+public struct CreateSyncSessionRequest {
+    public let alpha: Endpoint
+    public let beta: Endpoint
+
+    public init(alpha: Endpoint, beta: Endpoint) {
+        self.alpha = alpha
+        self.beta = beta
+    }
+}
+
+public struct Endpoint {
+    public let path: String
+    public let protocolKind: ProtocolKind
+
+    public init(path: String, protocolKind: ProtocolKind) {
+        self.path = path
+        self.protocolKind = protocolKind
+    }
+
+    public enum ProtocolKind {
+        case local
+        case ssh(host: String)
+    }
+
+    var mutagenURL: Url_URL {
+        switch protocolKind {
+        case .local:
+            .with { url in
+                url.path = path
+                url.protocol = .local
+            }
+        case let .ssh(host):
+            .with { url in
+                url.path = path
+                url.protocol = .ssh
+                url.host = host
+            }
+        }
+    }
+}
diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml
index fb38d35a..d2567673 100644
--- a/Coder-Desktop/project.yml
+++ b/Coder-Desktop/project.yml
@@ -164,7 +164,7 @@ targets:
         SKIP_INSTALL: NO
         LD_RUNPATH_SEARCH_PATHS:
           # Load frameworks from the SE bundle.
-          - "@executable_path/../../Contents/Library/SystemExtensions/com.coder.Coder-Desktop.VPN.systemextension/Contents/Frameworks" 
+          - "@executable_path/../../Contents/Library/SystemExtensions/com.coder.Coder-Desktop.VPN.systemextension/Contents/Frameworks"
           - "@executable_path/../Frameworks"
           - "@loader_path/Frameworks"
     dependencies:
@@ -192,6 +192,8 @@ targets:
     platform: macOS
     sources:
       - path: Coder-DesktopTests
+      - path: Resources
+        buildPhase: resources
     settings:
       base:
         BUNDLE_LOADER: "$(TEST_HOST)"
diff --git a/Makefile b/Makefile
index ebb8e384..115f6e89 100644
--- a/Makefile
+++ b/Makefile
@@ -116,7 +116,7 @@ fmt: ## Run Swift file formatter
 		$(FMTFLAGS) .
 
 .PHONY: test
-test: $(XCPROJECT) ## Run all tests
+test: $(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)) $(XCPROJECT) ## Run all tests
 	set -o pipefail && xcodebuild test \
 		-project $(XCPROJECT) \
 		-scheme $(SCHEME) \
