From 6abdb9f5bec6bcb135ff972b7c9291b8c93ed074 Mon Sep 17 00:00:00 2001 From: clopen-set <33433326+clopen-set@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:42:52 -0400 Subject: [PATCH 1/2] refactor(environments): route Swift client path sites through VellumPaths --- .../Security/FileCredentialStorage.swift | 7 ++- .../VellumPathsTests.swift | 46 ++++++++++++++----- clients/shared/App/Auth/DeviceIdStore.swift | 13 ++---- .../App/Auth/GuardianTokenFileReader.swift | 17 +++---- .../shared/App/Auth/SessionTokenManager.swift | 12 +---- .../shared/App/SigningIdentityManager.swift | 5 +- clients/shared/Utilities/LockfilePaths.swift | 17 ++----- 7 files changed, 55 insertions(+), 62 deletions(-) diff --git a/clients/macos/vellum-assistant/Security/FileCredentialStorage.swift b/clients/macos/vellum-assistant/Security/FileCredentialStorage.swift index bae9120d3ea..a4d7663f1b9 100644 --- a/clients/macos/vellum-assistant/Security/FileCredentialStorage.swift +++ b/clients/macos/vellum-assistant/Security/FileCredentialStorage.swift @@ -14,10 +14,9 @@ private let log = Logger( /// `~/.vellum/protected/credentials/` with 0600 permissions. struct FileCredentialStorage: CredentialStorage { - private static let credentialsDir: URL = { - let home = FileManager.default.homeDirectoryForCurrentUser - return home.appendingPathComponent(".vellum/protected/credentials") - }() + private static var credentialsDir: URL { + VellumPaths.current.credentialsDir + } /// Returns the file URL for a given credential account name. /// The account name is sanitized to a safe filename by replacing diff --git a/clients/macos/vellum-assistantTests/VellumPathsTests.swift b/clients/macos/vellum-assistantTests/VellumPathsTests.swift index 41398b9a90e..48f403914f3 100644 --- a/clients/macos/vellum-assistantTests/VellumPathsTests.swift +++ b/clients/macos/vellum-assistantTests/VellumPathsTests.swift @@ -123,19 +123,41 @@ final class VellumPathsTests: XCTestCase { ) } - // MARK: - Parity: Swift matches TS production paths byte-for-byte - + // MARK: - Parity: Swift matches pre-refactor inline paths byte-for-byte + + /// Backwards-compat anchor for PR 5. Every getter below MUST produce a path + /// byte-identical to the inline construction each consumer used before PR 5 + /// routed them through `VellumPaths.current`. If any of these assertions + /// change, production users will see a path shift — audit the consumer list + /// (`LockfilePaths`, `DeviceIdStore`, `SigningIdentityManager`, + /// `FileCredentialStorage`, `SessionTokenManager.xdgPlatformTokenPath`, + /// `GuardianTokenFileReader`) and ship a migration. func testProductionMatchesLegacyInlineConventions() { - // These paths MUST match what LockfilePaths.swift, DeviceIdStore.swift, - // SigningIdentityManager.swift, and FileCredentialStorage.swift - // currently construct inline. PR 5 routes those callers through - // VellumPaths.current and this parity must hold for production users - // to see zero path changes. let paths = makePaths(.production) - XCTAssertEqual(paths.lockfileCandidates[0].lastPathComponent, ".vellum.lock.json") - XCTAssertEqual(paths.lockfileCandidates[1].lastPathComponent, ".vellum.lockfile.json") - XCTAssertEqual(paths.deviceIdFile.path.hasSuffix("/.vellum/device.json"), true) - XCTAssertEqual(paths.signingKeyFile.path.hasSuffix("/.vellum/protected/app-signing-key"), true) - XCTAssertEqual(paths.credentialsDir.path.hasSuffix("/.vellum/protected/credentials"), true) + + XCTAssertEqual( + paths.lockfileCandidates[0].path, + "/tmp/test-home/.vellum.lock.json" + ) + XCTAssertEqual( + paths.lockfileCandidates[1].path, + "/tmp/test-home/.vellum.lockfile.json" + ) + XCTAssertEqual( + paths.deviceIdFile.path, + "/tmp/test-home/.vellum/device.json" + ) + XCTAssertEqual( + paths.signingKeyFile.path, + "/tmp/test-home/.vellum/protected/app-signing-key" + ) + XCTAssertEqual( + paths.credentialsDir.path, + "/tmp/test-home/.vellum/protected/credentials" + ) + XCTAssertEqual( + paths.platformTokenFile.path, + "/tmp/test-home/.config/vellum/platform-token" + ) } } diff --git a/clients/shared/App/Auth/DeviceIdStore.swift b/clients/shared/App/Auth/DeviceIdStore.swift index 301df0e3cf6..f92916b9171 100644 --- a/clients/shared/App/Auth/DeviceIdStore.swift +++ b/clients/shared/App/Auth/DeviceIdStore.swift @@ -27,9 +27,8 @@ public enum DeviceIdStore { if let cached { return cached } - let home = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) - let vellumDir = home.appendingPathComponent(".vellum", isDirectory: true) - let deviceFile = vellumDir.appendingPathComponent("device.json") + let deviceFile = VellumPaths.current.deviceIdFile + let vellumDir = deviceFile.deletingLastPathComponent() // 1. Try to read existing file (daemon or a previous run may have created it). if let data = try? Data(contentsOf: deviceFile), @@ -94,14 +93,8 @@ public enum DeviceIdStore { /// mirroring the daemon's `003-seed-device-id` migration so the same /// legacy ID is preserved regardless of whether macOS or daemon starts first. private static func installationIdFromLockfile() -> String? { - let home = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) - let candidates = [ - home.appendingPathComponent(".vellum.lock.json"), - home.appendingPathComponent(".vellum.lockfile.json"), - ] - var lockJSON: [String: Any]? - for candidate in candidates { + for candidate in VellumPaths.current.lockfileCandidates { guard let data = try? Data(contentsOf: candidate), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue diff --git a/clients/shared/App/Auth/GuardianTokenFileReader.swift b/clients/shared/App/Auth/GuardianTokenFileReader.swift index 56984b2532d..4705bb9fc51 100644 --- a/clients/shared/App/Auth/GuardianTokenFileReader.swift +++ b/clients/shared/App/Auth/GuardianTokenFileReader.swift @@ -4,7 +4,7 @@ import os private let log = Logger(subsystem: Bundle.appBundleIdentifier, category: "GuardianTokenFileReader") /// Reads guardian tokens persisted by the CLI at -/// `$XDG_CONFIG_HOME/vellum/assistants//guardian-token.json`. +/// `$XDG_CONFIG_HOME/vellum{-env}/assistants//guardian-token.json`. /// /// During non-local hatches (Docker, GCP, AWS, etc.) the CLI bootstraps the /// guardian token via `POST /v1/guardian/init` and writes the result to disk. @@ -119,17 +119,14 @@ public enum GuardianTokenFileReader { // MARK: - Path Resolution - /// Resolves `$XDG_CONFIG_HOME/vellum/assistants//guardian-token.json`, + /// Resolves `$XDG_CONFIG_HOME/vellum{-env}/assistants//guardian-token.json`, /// matching the CLI's `getGuardianTokenPath()`. private static func guardianTokenPath(for assistantId: String) -> String { - let configHome: String - if let xdg = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"]? - .trimmingCharacters(in: .whitespacesAndNewlines), !xdg.isEmpty { - configHome = xdg - } else { - configHome = NSHomeDirectory() + "/.config" - } - return "\(configHome)/vellum/assistants/\(assistantId)/guardian-token.json" + return VellumPaths.current.configDir + .appendingPathComponent("assistants") + .appendingPathComponent(assistantId) + .appendingPathComponent("guardian-token.json") + .path } // MARK: - Error Descriptions diff --git a/clients/shared/App/Auth/SessionTokenManager.swift b/clients/shared/App/Auth/SessionTokenManager.swift index ffdbea8466a..b946b622cfb 100644 --- a/clients/shared/App/Auth/SessionTokenManager.swift +++ b/clients/shared/App/Auth/SessionTokenManager.swift @@ -73,18 +73,10 @@ public enum SessionTokenManager { return nil } - /// XDG-compliant shared path (~/.config/vellum/platform-token). + /// Env-scoped shared path (`~/.config/vellum{-env}/platform-token`). /// Used by the CLI and desktop app as the canonical token location. private static func xdgPlatformTokenPath() -> String { - let launchEnvironment = ProcessInfo.processInfo.environment - let configHome: String - if let xdg = launchEnvironment["XDG_CONFIG_HOME"]?.trimmingCharacters(in: .whitespacesAndNewlines), - !xdg.isEmpty { - configHome = xdg - } else { - configHome = NSHomeDirectory() + "/.config" - } - return configHome + "/vellum/platform-token" + VellumPaths.current.platformTokenFile.path } private static func writePlatformTokenFile(_ token: String) { diff --git a/clients/shared/App/SigningIdentityManager.swift b/clients/shared/App/SigningIdentityManager.swift index 6a516ff4641..7cb709bc0bb 100644 --- a/clients/shared/App/SigningIdentityManager.swift +++ b/clients/shared/App/SigningIdentityManager.swift @@ -17,10 +17,9 @@ private let log = Logger( public actor SigningIdentityManager { public static let shared = SigningIdentityManager() - /// File path for the signing key: ~/.vellum/protected/app-signing-key + /// File path for the signing key, resolved via `VellumPaths.current`. private var keyFilePath: URL { - let home = FileManager.default.homeDirectoryForCurrentUser - return home.appendingPathComponent(".vellum/protected/app-signing-key") + VellumPaths.current.signingKeyFile } /// Cached private key to avoid repeated file reads. diff --git a/clients/shared/Utilities/LockfilePaths.swift b/clients/shared/Utilities/LockfilePaths.swift index 391e79a4b65..61e9203749d 100644 --- a/clients/shared/Utilities/LockfilePaths.swift +++ b/clients/shared/Utilities/LockfilePaths.swift @@ -1,25 +1,16 @@ import Foundation public enum LockfilePaths { - private static var homeDir: URL { - URL(fileURLWithPath: NSHomeDirectory()) - } - public static var primary: URL { - homeDir.appendingPathComponent(".vellum.lock.json") - } - - public static var legacy: URL { - homeDir.appendingPathComponent(".vellum.lockfile.json") + VellumPaths.current.lockfileCandidates[0] } public static var primaryPath: String { primary.path } - /// Read and parse the lockfile, trying the primary path first, - /// then falling back to the legacy path. - /// Returns nil if neither file exists or both are malformed. + /// Read and parse the lockfile, iterating `VellumPaths.current.lockfileCandidates` + /// in priority order. Returns nil if no candidate exists or all are malformed. public static func read() -> [String: Any]? { - for url in [primary, legacy] { + for url in VellumPaths.current.lockfileCandidates { guard let data = try? Data(contentsOf: url), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue From 29608945b37ec2e5a754c1263481b66bf08daec6 Mon Sep 17 00:00:00 2001 From: clopen-set <33433326+clopen-set@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:24:00 -0400 Subject: [PATCH 2/2] fix(environments): remove dead xdgDataHome field + reject relative XDG paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Devin and Codex P2 findings on PR #25457: - xdgDataHome was stored but never read; remove the field and its resolver helper - resolveXdgConfigHome() no longer rewrites relative XDG_CONFIG_HOME values against cwd — relative values are rejected for parity with the TypeScript env package --- .../VellumPathsTests.swift | 4 +-- clients/shared/Utilities/VellumPaths.swift | 26 +++++-------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/clients/macos/vellum-assistantTests/VellumPathsTests.swift b/clients/macos/vellum-assistantTests/VellumPathsTests.swift index 48f403914f3..2910f41bd7a 100644 --- a/clients/macos/vellum-assistantTests/VellumPathsTests.swift +++ b/clients/macos/vellum-assistantTests/VellumPathsTests.swift @@ -6,14 +6,12 @@ final class VellumPathsTests: XCTestCase { // Explicit test roots so we don't depend on process environment private let testHome = URL(fileURLWithPath: "/tmp/test-home") private let testXdgConfig = URL(fileURLWithPath: "/tmp/test-home/.config") - private let testXdgData = URL(fileURLWithPath: "/tmp/test-home/.local/share") private func makePaths(_ env: VellumEnvironment) -> VellumPaths { VellumPaths( environment: env, homeDirectory: testHome, - xdgConfigHome: testXdgConfig, - xdgDataHome: testXdgData + xdgConfigHome: testXdgConfig ) } diff --git a/clients/shared/Utilities/VellumPaths.swift b/clients/shared/Utilities/VellumPaths.swift index a828074b7a3..16eb279a15b 100644 --- a/clients/shared/Utilities/VellumPaths.swift +++ b/clients/shared/Utilities/VellumPaths.swift @@ -22,7 +22,6 @@ public struct VellumPaths { public let environment: VellumEnvironment public let homeDirectory: URL public let xdgConfigHome: URL - public let xdgDataHome: URL /// Resolved path bundle for the current process environment. /// @@ -33,21 +32,18 @@ public struct VellumPaths { VellumPaths( environment: .current, homeDirectory: URL(fileURLWithPath: NSHomeDirectory()), - xdgConfigHome: Self.resolveXdgConfigHome(), - xdgDataHome: Self.resolveXdgDataHome() + xdgConfigHome: Self.resolveXdgConfigHome() ) }() public init( environment: VellumEnvironment, homeDirectory: URL, - xdgConfigHome: URL, - xdgDataHome: URL + xdgConfigHome: URL ) { self.environment = environment self.homeDirectory = homeDirectory self.xdgConfigHome = xdgConfigHome - self.xdgDataHome = xdgDataHome } // MARK: - Path getters @@ -113,23 +109,15 @@ public struct VellumPaths { private static func resolveXdgConfigHome() -> URL { if let raw = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"]? .trimmingCharacters(in: .whitespacesAndNewlines), - !raw.isEmpty + !raw.isEmpty, + // XDG Base Directory spec requires absolute paths; relative values + // are ignored for parity with the TypeScript env package which + // also doesn't resolve them. + raw.hasPrefix("/") { return URL(fileURLWithPath: raw) } return URL(fileURLWithPath: NSHomeDirectory()) .appendingPathComponent(".config") } - - private static func resolveXdgDataHome() -> URL { - if let raw = ProcessInfo.processInfo.environment["XDG_DATA_HOME"]? - .trimmingCharacters(in: .whitespacesAndNewlines), - !raw.isEmpty - { - return URL(fileURLWithPath: raw) - } - return URL(fileURLWithPath: NSHomeDirectory()) - .appendingPathComponent(".local") - .appendingPathComponent("share") - } }