From d696afe5fef9e85d714d8f85189a418e54259f55 Mon Sep 17 00:00:00 2001 From: f-trycua Date: Tue, 10 Feb 2026 22:03:33 -0800 Subject: [PATCH] fix(lume): disable clipboard sync by default, add --clipboard opt-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clipboard sync is experimental and creates noisy SSH error logs when the VM's SSH is unavailable. Disable it by default and add a --clipboard flag to opt in. - Add --clipboard flag to `lume run` (default: off) - Add `clipboard` field to the HTTP API RunVMRequest - Add `clipboard` param to the MCP server run handler - Thread the flag through LumeController → VM.run - Update API docs Closes #1054 --- libs/lume/src/Commands/Run.swift | 6 +++++- libs/lume/src/LumeController.swift | 6 ++++-- libs/lume/src/Server/APIDocExtractor.swift | 3 ++- libs/lume/src/Server/Handlers.swift | 11 +++++++---- libs/lume/src/Server/MCPServer.swift | 4 +++- libs/lume/src/Server/Requests.swift | 1 + libs/lume/src/VM/VM.swift | 8 +++++--- 7 files changed, 27 insertions(+), 12 deletions(-) diff --git a/libs/lume/src/Commands/Run.swift b/libs/lume/src/Commands/Run.swift index dcf4df54..b7188ec9 100644 --- a/libs/lume/src/Commands/Run.swift +++ b/libs/lume/src/Commands/Run.swift @@ -56,6 +56,9 @@ struct Run: AsyncParsableCommand { help: "Optional network override: 'nat', 'bridged', or 'bridged:' (e.g. 'bridged:en0'). Defaults to the VM's configured mode.") var network: String? + @Flag(name: .customLong("clipboard"), help: "Enable bidirectional clipboard sync with the VM via SSH (experimental)") + var clipboard: Bool = false + private var parsedNetworkMode: NetworkMode? { get throws { guard let network else { @@ -133,7 +136,8 @@ struct Run: AsyncParsableCommand { recoveryMode: recoveryMode, storage: storage, usbMassStoragePaths: parsedUSBStorageDevices.isEmpty ? nil : parsedUSBStorageDevices, - networkMode: parsedNetworkMode + networkMode: parsedNetworkMode, + clipboard: clipboard ) } } diff --git a/libs/lume/src/LumeController.swift b/libs/lume/src/LumeController.swift index 2608a8f6..88923834 100644 --- a/libs/lume/src/LumeController.swift +++ b/libs/lume/src/LumeController.swift @@ -900,7 +900,8 @@ final class LumeController { recoveryMode: Bool = false, storage: String? = nil, usbMassStoragePaths: [Path]? = nil, - networkMode: NetworkMode? = nil + networkMode: NetworkMode? = nil, + clipboard: Bool = false ) async throws { let normalizedName = normalizeVMName(name: name) Logger.info( @@ -998,7 +999,8 @@ final class LumeController { vncPort: vncPort, recoveryMode: recoveryMode, usbMassStoragePaths: usbMassStoragePaths, - networkMode: networkMode) + networkMode: networkMode, + clipboard: clipboard) Logger.info("VM started successfully", metadata: ["name": normalizedName]) } catch { SharedVM.shared.removeVM(name: normalizedName) diff --git a/libs/lume/src/Server/APIDocExtractor.swift b/libs/lume/src/Server/APIDocExtractor.swift index 0e6c722d..723f8afd 100644 --- a/libs/lume/src/Server/APIDocExtractor.swift +++ b/libs/lume/src/Server/APIDocExtractor.swift @@ -306,7 +306,8 @@ enum APIDocExtractor { APIFieldDoc(name: "noDisplay", type: "boolean", required: false, description: "Run without VNC display", defaultValue: "false"), APIFieldDoc(name: "sharedDirectories", type: "array", required: false, description: "Directories to share with the VM", defaultValue: nil), APIFieldDoc(name: "recoveryMode", type: "boolean", required: false, description: "Boot macOS VM in recovery mode", defaultValue: "false"), - APIFieldDoc(name: "storage", type: "string", required: false, description: "VM storage location", defaultValue: nil) + APIFieldDoc(name: "storage", type: "string", required: false, description: "VM storage location", defaultValue: nil), + APIFieldDoc(name: "clipboard", type: "boolean", required: false, description: "Enable bidirectional clipboard sync via SSH (experimental)", defaultValue: "false") ] ), responseBody: APIResponseDoc( diff --git a/libs/lume/src/Server/Handlers.swift b/libs/lume/src/Server/Handlers.swift index 5c49eb3c..c590a13f 100644 --- a/libs/lume/src/Server/Handlers.swift +++ b/libs/lume/src/Server/Handlers.swift @@ -340,7 +340,7 @@ extension Server { body.flatMap { try? JSONDecoder().decode(RunVMRequest.self, from: $0) } ?? RunVMRequest( noDisplay: nil, sharedDirectories: nil, recoveryMode: nil, storage: nil, - network: nil) + network: nil, clipboard: nil) // Record telemetry TelemetryClient.shared.record(event: TelemetryEvent.apiVMRun, properties: [ @@ -372,7 +372,8 @@ extension Server { sharedDirectories: dirs, recoveryMode: request.recoveryMode ?? false, storage: request.storage, - networkMode: networkMode + networkMode: networkMode, + clipboard: request.clipboard ?? false ) Logger.info("VM start initiated in background", metadata: ["name": name]) @@ -825,7 +826,8 @@ extension Server { sharedDirectories: [SharedDirectory] = [], recoveryMode: Bool = false, storage: String? = nil, - networkMode: NetworkMode? = nil + networkMode: NetworkMode? = nil, + clipboard: Bool = false ) { Logger.info( "Starting VM in detached task", @@ -855,7 +857,8 @@ extension Server { sharedDirectories: sharedDirectories, recoveryMode: recoveryMode, storage: storage, - networkMode: networkMode + networkMode: networkMode, + clipboard: clipboard ) Logger.info("VM started successfully in background task", metadata: ["name": name]) } catch { diff --git a/libs/lume/src/Server/MCPServer.swift b/libs/lume/src/Server/MCPServer.swift index 2a7c65ab..492a6465 100644 --- a/libs/lume/src/Server/MCPServer.swift +++ b/libs/lume/src/Server/MCPServer.swift @@ -565,6 +565,7 @@ final class LumeMCPServer { let storage = args?["storage"]?.stringValue let noDisplay = args?["no_display"]?.boolValue ?? true + let clipboard = args?["clipboard"]?.boolValue ?? false var sharedDirectories: [SharedDirectory] = [] if let sharedDir = args?["shared_dir"]?.stringValue { @@ -581,7 +582,8 @@ final class LumeMCPServer { name: name, noDisplay: noDisplay, sharedDirectories: sharedDirectories, - storage: storage + storage: storage, + clipboard: clipboard ) } catch { Logger.error( diff --git a/libs/lume/src/Server/Requests.swift b/libs/lume/src/Server/Requests.swift index 1a39bdb5..7778ce83 100644 --- a/libs/lume/src/Server/Requests.swift +++ b/libs/lume/src/Server/Requests.swift @@ -17,6 +17,7 @@ struct RunVMRequest: Codable { let recoveryMode: Bool? let storage: String? let network: String? + let clipboard: Bool? struct SharedDirectoryRequest: Codable { let hostPath: String diff --git a/libs/lume/src/VM/VM.swift b/libs/lume/src/VM/VM.swift index 9716b4a9..014ebf48 100644 --- a/libs/lume/src/VM/VM.swift +++ b/libs/lume/src/VM/VM.swift @@ -136,7 +136,7 @@ class VM { func run( noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?, vncPort: Int = 0, recoveryMode: Bool = false, usbMassStoragePaths: [Path]? = nil, - networkMode: NetworkMode? = nil + networkMode: NetworkMode? = nil, clipboard: Bool = false ) async throws { Logger.info( "VM.run method called", @@ -261,8 +261,10 @@ class VM { // Start clipboard watcher for automatic host-to-VM clipboard sync // Requires SSH/Remote Login to be enabled on the VM - clipboardWatcher = ClipboardWatcher(vmName: vmDirContext.name, storage: vmDirContext.storage) - await clipboardWatcher?.start() + if clipboard { + clipboardWatcher = ClipboardWatcher(vmName: vmDirContext.name, storage: vmDirContext.storage) + await clipboardWatcher?.start() + } while true { try await Task.sleep(nanoseconds: UInt64(1e9))