Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 34 additions & 14 deletions clients/macos/vellum-assistantTests/VellumPathsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}

Expand Down Expand Up @@ -123,19 +121,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"
)
}
}
13 changes: 3 additions & 10 deletions clients/shared/App/Auth/DeviceIdStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
clopen-set marked this conversation as resolved.
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),
Expand Down Expand Up @@ -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
Expand Down
17 changes: 7 additions & 10 deletions clients/shared/App/Auth/GuardianTokenFileReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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/<assistantId>/guardian-token.json`.
/// `$XDG_CONFIG_HOME/vellum{-env}/assistants/<assistantId>/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.
Expand Down Expand Up @@ -119,17 +119,14 @@ public enum GuardianTokenFileReader {

// MARK: - Path Resolution

/// Resolves `$XDG_CONFIG_HOME/vellum/assistants/<id>/guardian-token.json`,
/// Resolves `$XDG_CONFIG_HOME/vellum{-env}/assistants/<id>/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
Expand Down
12 changes: 2 additions & 10 deletions clients/shared/App/Auth/SessionTokenManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
clopen-set marked this conversation as resolved.
}

private static func writePlatformTokenFile(_ token: String) {
Expand Down
5 changes: 2 additions & 3 deletions clients/shared/App/SigningIdentityManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 4 additions & 13 deletions clients/shared/Utilities/LockfilePaths.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
26 changes: 7 additions & 19 deletions clients/shared/Utilities/VellumPaths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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
Expand Down Expand Up @@ -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")
}
}