This commit is contained in:
Max Goedjen
2025-12-14 11:17:28 -08:00
34 changed files with 2413 additions and 768 deletions

View File

@@ -25,6 +25,9 @@ let package = Package(
.library(
name: "SecretAgentKit",
targets: ["SecretAgentKit"]),
.library(
name: "Common",
targets: ["Common"]),
.library(
name: "Brief",
targets: ["Brief"]),
@@ -63,7 +66,7 @@ let package = Package(
),
.target(
name: "CertificateKit",
dependencies: ["SecretKit"],
dependencies: ["SecretKit", "SSHProtocolKit"],
resources: [localization],
// swiftSettings: swiftSettings,
),
@@ -87,6 +90,12 @@ let package = Package(
name: "SSHProtocolKitTests",
dependencies: ["SSHProtocolKit"],
),
.target(
name: "Common",
dependencies: [],
resources: [localization],
swiftSettings: swiftSettings,
),
.target(
name: "Brief",
dependencies: ["XPCWrappers", "SSHProtocolKit"],

File diff suppressed because it is too large Load Diff

View File

@@ -2,19 +2,23 @@ import Foundation
extension URL {
static var agentHomeURL: URL {
public static var agentHomeURL: URL {
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
}
static var socketPath: String {
public static var socketPath: String {
#if DEBUG
URL.agentHomeURL.appendingPathComponent("socket-debug.ssh").path()
#else
URL.agentHomeURL.appendingPathComponent("socket.ssh").path()
#endif
}
}
extension String {
var normalizedPathAndFolder: (String, String) {
public var normalizedPathAndFolder: (String, String) {
// All foundation-based normalization methods replace this with the container directly.
let processedPath = replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
let url = URL(filePath: processedPath)

View File

@@ -48,6 +48,7 @@ extension Agent {
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
case .unknown(let value):
logger.error("Agent received unknown request of type \(value).")
throw UnhandledRequestError()
default:
logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.")
throw UnhandledRequestError()

View File

@@ -36,7 +36,7 @@ extension SigningRequestTracer {
/// - Parameter pid: The process ID to look up.
/// - Returns: A ``SecretKit.SigningRequestProvenance.Process`` describing the process.
func process(from pid: Int32) -> SigningRequestProvenance.Process {
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
var pidAndNameInfo = unsafe self.pidAndNameInfo(from: pid)
let ppid = unsafe pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
let procName = unsafe withUnsafeMutablePointer(to: &pidAndNameInfo.kp_proc.p_comm.0) { pointer in
unsafe String(cString: pointer)

View File

@@ -36,16 +36,21 @@ public struct SocketController {
logger.debug("Socket controller path is clear")
port = SocketPort(path: path)
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
Task { [fileHandle, sessionsContinuation, logger] in
for await notification in NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted) {
Task { @MainActor [fileHandle, sessionsContinuation, logger] in
// Create the sequence before triggering the notification to
// ensure it will not be missed.
let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted)
fileHandle.acceptConnectionInBackgroundAndNotify()
for await notification in connectionAcceptedNotifications {
logger.debug("Socket controller accepted connection")
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { continue }
let session = Session(fileHandle: new)
sessionsContinuation.yield(session)
await fileHandle.acceptConnectionInBackgroundAndNotifyOnMainActor()
fileHandle.acceptConnectionInBackgroundAndNotify()
}
}
fileHandle.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.Mode.common])
logger.debug("Socket listening at \(path)")
}
@@ -77,8 +82,14 @@ extension SocketController {
self.fileHandle = fileHandle
provenance = SigningRequestTracer().provenance(from: fileHandle)
(messages, messagesContinuation) = AsyncStream.makeStream()
Task { [messagesContinuation, logger] in
for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) {
Task { @MainActor [messagesContinuation, logger] in
// Create the sequence before triggering the notification to
// ensure it will not be missed.
let dataAvailableNotifications = NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle)
fileHandle.waitForDataInBackgroundAndNotify()
for await _ in dataAvailableNotifications {
let data = fileHandle.availableData
guard !data.isEmpty else {
logger.debug("Socket controller received empty data, ending continuation.")
@@ -90,16 +101,13 @@ extension SocketController {
logger.debug("Socket controller yielded data.")
}
}
Task {
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
}
}
/// Writes new data to the socket.
/// - Parameter data: The data to write.
public func write(_ data: Data) async throws {
try fileHandle.write(contentsOf: data)
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
@MainActor public func write(_ data: Data) throws {
try fileHandle.write(contentsOf: data)
fileHandle.waitForDataInBackgroundAndNotify()
}
/// Closes the socket and cleans up resources.
@@ -113,22 +121,6 @@ extension SocketController {
}
private extension FileHandle {
/// Ensures waitForDataInBackgroundAndNotify will be called on the main actor.
@MainActor func waitForDataInBackgroundAndNotifyOnMainActor() {
waitForDataInBackgroundAndNotify()
}
/// Ensures acceptConnectionInBackgroundAndNotify will be called on the main actor.
/// - Parameter modes: the runloop modes to use.
@MainActor func acceptConnectionInBackgroundAndNotifyOnMainActor(forModes modes: [RunLoop.Mode]? = [RunLoop.Mode.common]) {
acceptConnectionInBackgroundAndNotify(forModes: modes)
}
}
private extension SocketPort {
convenience init(path: String) {

View File

@@ -8,6 +8,7 @@ import Brief
import Observation
import SSHProtocolKit
import CertificateKit
import Common
@main
class AppDelegate: NSObject, NSApplicationDelegate {
@@ -30,7 +31,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
Agent(storeList: storeList, witness: notifier)
}()
private lazy var socketController: SocketController = {
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
let path = URL.socketPath as String
return SocketController(path: path)
}()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
@@ -45,7 +46,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
for await message in session.messages {
let request = try await inputParser.parse(data: message)
let agentResponse = await agent.handle(request: request, provenance: session.provenance)
try await session.write(agentResponse)
try session.write(agentResponse)
}
} catch {
try session.close()

View File

@@ -9,7 +9,6 @@
/* Begin PBXBuildFile section */
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; };
50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; };
50033AC327813F1700253856 /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50033AC227813F1700253856 /* BundleIDs.swift */; };
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; };
5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; };
5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3E278005F300DF2006 /* SecretAgentKit */; };
@@ -26,13 +25,11 @@
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; };
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; };
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
505993512E7E59FB0092CFFA /* XPCCertificateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505993502E7E59F70092CFFA /* XPCCertificateParser.swift */; };
505993532E7E70C90092CFFA /* CertificateKit in Frameworks */ = {isa = PBXBuildFile; productRef = 505993522E7E70C90092CFFA /* CertificateKit */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
@@ -71,6 +68,8 @@
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; };
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; };
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; };
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; };
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
@@ -197,20 +196,18 @@
/* Begin PBXFileReference section */
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = "<group>"; };
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/Localizable.xcstrings; sourceTree = SOURCE_ROOT; };
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = "<group>"; };
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
5059933F2E7A3B5B0092CFFA /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = "<group>"; };
505993502E7E59F70092CFFA /* XPCCertificateParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCCertificateParser.swift; sourceTree = "<group>"; };
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
@@ -244,7 +241,6 @@
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; };
50A3B78A24026B7500D209EA /* SecretAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecretAgent.app; sourceTree = BUILT_PRODUCTS_DIR; };
50A3B79324026B7600D209EA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = "<group>"; };
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = "<group>"; };
@@ -272,6 +268,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
501421622781262300BBAA70 /* Brief in Frameworks */,
505993532E7E70C90092CFFA /* CertificateKit in Frameworks */,
@@ -307,6 +304,7 @@
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */,
5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */,
5003EF41278005FA00DF2006 /* SecretKit in Frameworks */,
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -322,14 +320,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
50033AC427813F1C00253856 /* Helpers */ = {
isa = PBXGroup;
children = (
50033AC227813F1700253856 /* BundleIDs.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
504788ED2E681EB200B4556F /* Modifiers */ = {
isa = PBXGroup;
children = (
@@ -415,7 +405,6 @@
50617D8223FCE48E0099B055 /* App.swift */,
508A58B0241ED1C40069DC07 /* Views */,
508A58B1241ED1EA0069DC07 /* Controllers */,
50033AC427813F1C00253856 /* Helpers */,
50617D8E23FCE48E0099B055 /* Info.plist */,
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
50E4C4C72E777E4200C73783 /* AppIcon.icon */,
@@ -483,11 +472,9 @@
508A58B1241ED1EA0069DC07 /* Controllers */ = {
isa = PBXGroup;
children = (
504788EB2E680DC400B4556F /* URLs.swift */,
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
505993502E7E59F70092CFFA /* XPCCertificateParser.swift */,
);
path = Controllers;
@@ -563,6 +550,7 @@
5003EF602780081600DF2006 /* SmartCardSecretKit */,
501421612781262300BBAA70 /* Brief */,
505993522E7E70C90092CFFA /* CertificateKit */,
50E0145B2EDB9CDF00B121F1 /* Common */,
);
productName = Secretive;
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
@@ -634,6 +622,7 @@
5003EF40278005FA00DF2006 /* SecretKit */,
5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */,
5003EF642780081B00DF2006 /* SmartCardSecretKit */,
50E0145D2EDB9CE400B121F1 /* Common */,
);
productName = SecretAgent;
productReference = 50A3B78A24026B7500D209EA /* SecretAgent.app */;
@@ -694,7 +683,6 @@
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
it,
fr,
de,
@@ -777,7 +765,6 @@
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
@@ -788,14 +775,12 @@
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
50033AC327813F1700253856 /* BundleIDs.swift in Sources */,
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */,
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
@@ -889,7 +874,7 @@
50A3B79524026B7600D209EA /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
50A3B79624026B7600D209EA /* Base */,
5059933F2E7A3B5B0092CFFA /* en */,
);
name = Main.storyboard;
sourceTree = "<group>";
@@ -1539,6 +1524,14 @@
isa = XCSwiftPackageProductDependency;
productName = Brief;
};
50E0145B2EDB9CDF00B121F1 /* Common */ = {
isa = XCSwiftPackageProductDependency;
productName = Common;
};
50E0145D2EDB9CE400B121F1 /* Common */ = {
isa = XCSwiftPackageProductDependency;
productName = Common;
};
50E4C4EC2E77C55E00C73783 /* XPCWrappers */ = {
isa = XCSwiftPackageProductDependency;
productName = XPCWrappers;

View File

@@ -8,7 +8,7 @@ import CertificateKit
@main
struct Secretive: App {
@Environment(\.agentStatusChecker) var agentStatusChecker
@Environment(\.agentLaunchController) var agentLaunchController
@Environment(\.justUpdatedChecker) var justUpdatedChecker
@SceneBuilder var body: some Scene {
@@ -17,14 +17,16 @@ struct Secretive: App {
.environment(EnvironmentValues._secretStoreList)
.environment(EnvironmentValues._certificateStore)
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
guard hasRunSetup else { return }
agentStatusChecker.check()
if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild {
// Relaunch the agent, since it'll be running from earlier update still
reinstallAgent()
} else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild {
forceLaunchAgent()
Task {
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
guard hasRunSetup && !explicitlyDisabled else { return }
agentLaunchController.check()
guard !agentLaunchController.developmentBuild else { return }
if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
// Relaunch the agent, since it'll be running from earlier update still
try await agentLaunchController.forceLaunch()
}
}
}
}
@@ -81,30 +83,6 @@ extension Secretive {
}
extension Secretive {
private func reinstallAgent() {
Task {
_ = await LaunchAgentController().install()
try? await Task.sleep(for: .seconds(1))
agentStatusChecker.check()
if !agentStatusChecker.running {
forceLaunchAgent()
}
}
}
private func forceLaunchAgent() {
// We've run setup, we didn't just update, launchd is just not doing it's thing.
// Force a launch directly.
Task {
_ = await LaunchAgentController().forceLaunch()
agentStatusChecker.check()
}
}
}
private enum Constants {
static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
}
@@ -124,9 +102,10 @@ extension EnvironmentValues {
}()
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
private static let _agentLaunchController = AgentLaunchController()
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
private static let _agentStatusChecker = AgentStatusChecker()
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
private static let _updater: any UpdaterProtocol = {
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
return Updater(checkOnLaunch: hasRunSetup)

View File

@@ -2,18 +2,26 @@ import Foundation
import AppKit
import SecretKit
import Observation
import OSLog
import ServiceManagement
import Common
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
@MainActor protocol AgentLaunchControllerProtocol: Observable, Sendable {
var running: Bool { get }
var developmentBuild: Bool { get }
var process: NSRunningApplication? { get }
func check()
func install() async throws
func uninstall() async throws
func forceLaunch() async throws
}
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
@Observable @MainActor final class AgentLaunchController: AgentLaunchControllerProtocol {
var running: Bool = false
var process: NSRunningApplication? = nil
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
private let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
nonisolated init() {
Task { @MainActor in
@@ -33,7 +41,7 @@ import Observation
// The process corresponding to this instance of Secretive
var instanceSecretAgentProcess: NSRunningApplication? {
// FIXME: CHECK VERSION
// TODO: CHECK VERSION
let agents = allSecretAgentProcesses
for agent in agents {
guard let url = agent.bundleURL else { continue }
@@ -49,6 +57,47 @@ import Observation
Bundle.main.bundleURL.isXcodeURL
}
func install() async throws {
logger.debug("Installing agent")
try? await service.unregister()
// This is definitely a bit of a "seems to work better" thing but:
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new?
try await Task.sleep(for: .seconds(1))
try service.register()
try await Task.sleep(for: .seconds(1))
check()
}
func uninstall() async throws {
logger.debug("Uninstalling agent")
try await Task.sleep(for: .seconds(1))
try await service.unregister()
try await Task.sleep(for: .seconds(1))
check()
}
func forceLaunch() async throws {
logger.debug("Agent is not running, attempting to force launch by reinstalling")
try await install()
if running {
logger.debug("Agent successfully force launched by reinstalling")
return
}
logger.debug("Agent is not running, attempting to force launch by launching directly")
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration()
config.activates = false
do {
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
logger.debug("Agent force launched")
try await Task.sleep(for: .seconds(1))
} catch {
logger.error("Error force launching \(error.localizedDescription)")
}
check()
}
}
extension URL {

View File

@@ -1,65 +0,0 @@
import Foundation
import ServiceManagement
import AppKit
import OSLog
import SecretKit
struct LaunchAgentController {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
func install() async -> Bool {
logger.debug("Installing agent")
_ = setEnabled(false)
// This is definitely a bit of a "seems to work better" thing but:
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new?
try? await Task.sleep(for: .seconds(1))
let result = await MainActor.run {
setEnabled(true)
}
try? await Task.sleep(for: .seconds(1))
return result
}
func uninstall() async -> Bool {
logger.debug("Uninstalling agent")
try? await Task.sleep(for: .seconds(1))
let result = await MainActor.run {
setEnabled(false)
}
try? await Task.sleep(for: .seconds(1))
return result
}
func forceLaunch() async -> Bool {
logger.debug("Agent is not running, attempting to force launch")
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration()
config.activates = false
do {
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
logger.debug("Agent force launched")
try? await Task.sleep(for: .seconds(1))
return true
} catch {
logger.error("Error force launching \(error.localizedDescription)")
return false
}
}
private func setEnabled(_ enabled: Bool) -> Bool {
let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
do {
if enabled {
try service.register()
} else {
try service.unregister()
}
return true
} catch {
return false
}
}
}

View File

@@ -20,12 +20,12 @@
<string>$(CI_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CI_BUILD_NUMBER)</string>
<key>GitHubBuildLog</key>
<string>https://$(CI_BUILD_LINK)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
<key>GitHubBuildLog</key>
<string>$(CI_BUILD_LINK)</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key>

View File

@@ -1,7 +1,7 @@
import Foundation
import AppKit
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
class PreviewAgentLaunchController: AgentLaunchControllerProtocol {
let running: Bool
let process: NSRunningApplication?
@@ -15,4 +15,13 @@ class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
func check() {
}
func install() async throws {
}
func uninstall() async throws {
}
func forceLaunch() async throws {
}
}

View File

@@ -3,6 +3,7 @@ import SwiftUI
struct SetupView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.agentLaunchController) private var agentLaunchController
@Binding var setupComplete: Bool
@State var showingIntegrations = false
@@ -31,7 +32,7 @@ struct SetupView: View {
) {
installed = true
Task {
await LaunchAgentController().install()
try? await agentLaunchController.install()
}
}
}

View File

@@ -11,6 +11,7 @@ struct ToolConfigurationView: View {
@State var creating = false
@State var selectedSecret: AnySecret?
@State var email = ""
init(selectedInstruction: ConfigurationFileInstructions) {
self.selectedInstruction = selectedInstruction
@@ -49,6 +50,12 @@ struct ToolConfigurationView: View {
.tag(secret)
}
}
TextField(text: $email, prompt: Text(.integrationsConfigureUsingEmailPlaceholder)) {
Text(.integrationsConfigureUsingEmailTitle)
Text(.integrationsConfigureUsingEmailSubtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
} header: {
Text(.integrationsConfigureUsingSecretHeader)
}
@@ -61,7 +68,7 @@ struct ToolConfigurationView: View {
Section {
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
ForEach(stepGroup.steps, id: \.self.key) { step in
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) {
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(placeholdersReplaced(text: String(localized: step)))) {
HStack {
Text(placeholdersReplaced(text: String(localized: step)))
.padding(8)
@@ -103,9 +110,11 @@ struct ToolConfigurationView: View {
func placeholdersReplaced(text: String) -> String {
guard let selectedSecret else { return text }
let writer = OpenSSHPublicKeyWriter()
let gitAllowedSignersString = [email.isEmpty ? String(localized: .integrationsConfigureUsingEmailPlaceholder) : email, writer.openSSHString(secret: selectedSecret)]
.joined(separator: " ")
let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
return text
.replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret))
.replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: gitAllowedSignersString)
.replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret))
}

View File

@@ -68,6 +68,9 @@ struct ToolbarButtonStyle: PrimitiveButtonStyle {
.padding(.vertical, 10)
.padding(.horizontal, 12)
.glassEffect(.regular.interactive().tint(tint))
.onTapGesture {
configuration.trigger()
}
} else {
BorderedButtonStyle().makeBody(configuration: configuration)
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))

View File

@@ -24,6 +24,18 @@ struct SecretListItemView: View {
Text(secret.name)
}
}
.sheet(isPresented: $isRenaming, onDismiss: {
renamedSecret(secret)
}, content: {
if let modifiable = store as? AnySecretStoreModifiable {
EditSecretView(store: modifiable, secret: secret)
}
})
.showingDeleteConfirmation(isPresented: $isDeleting, secret, store as? AnySecretStoreModifiable) { deleted in
if deleted {
deletedSecret(secret)
}
}
.contextMenu {
if store is AnySecretStoreModifiable {
Button(action: { isRenaming = true }) {
@@ -36,17 +48,5 @@ struct SecretListItemView: View {
}
}
}
.showingDeleteConfirmation(isPresented: $isDeleting, secret, store as? AnySecretStoreModifiable) { deleted in
if deleted {
deletedSecret(secret)
}
}
.sheet(isPresented: $isRenaming, onDismiss: {
renamedSecret(secret)
}, content: {
if let modifiable = store as? AnySecretStoreModifiable {
EditSecretView(store: modifiable, secret: secret)
}
})
}
}

View File

@@ -2,10 +2,10 @@ import SwiftUI
struct AgentStatusView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
var body: some View {
if agentStatusChecker.running {
if agentLaunchController.running {
AgentRunningView()
} else {
AgentNotRunningView()
@@ -14,12 +14,13 @@ struct AgentStatusView: View {
}
struct AgentRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
var body: some View {
Form {
Section {
if let process = agentStatusChecker.process {
if let process = agentLaunchController.process {
ConfigurationItemView(
title: .agentDetailsLocationTitle,
value: process.bundleURL!.path(),
@@ -53,19 +54,14 @@ struct AgentRunningView: View {
Menu(.agentDetailsRestartAgentButton) {
Button(.agentDetailsDisableAgentButton) {
Task {
_ = await LaunchAgentController()
explicitlyDisabled = true
try? await agentLaunchController
.uninstall()
agentStatusChecker.check()
}
}
} primaryAction: {
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
try? await agentLaunchController.forceLaunch()
}
}
}
@@ -82,9 +78,10 @@ struct AgentRunningView: View {
struct AgentNotRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@Environment(\.agentLaunchController) private var agentLaunchController
@State var triedRestart = false
@State var loading = false
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
var body: some View {
Form {
@@ -100,18 +97,14 @@ struct AgentNotRunningView: View {
if !triedRestart {
Spacer()
Button {
explicitlyDisabled = false
guard !loading else { return }
loading = true
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
try await agentLaunchController.forceLaunch()
loading = false
if !agentStatusChecker.running {
if !agentLaunchController.running {
triedRestart = true
}
}
@@ -145,9 +138,9 @@ struct AgentNotRunningView: View {
//#Preview {
// AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
// .environment(\.agentLaunchController, PreviewAgentLaunchController(running: false))
//}
//#Preview {
// AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
// .environment(\.agentLaunchController, PreviewAgentLaunchController(running: true, process: .current))
//}

View File

@@ -16,7 +16,7 @@ struct ContentView: View {
@Environment(\.secretStoreList) private var storeList
@Environment(\.certificateStore) private var certificateStore
@Environment(\.updater) private var updater
@Environment(\.agentStatusChecker) private var agentStatusChecker
@Environment(\.agentLaunchController) private var agentLaunchController
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
@State private var showingCreation = false
@@ -145,7 +145,7 @@ extension ContentView {
showingAgentInfo = true
}, label: {
HStack {
if agentStatusChecker.running {
if agentLaunchController.running {
Text(.agentRunningNoticeTitle)
.font(.headline)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
@@ -163,8 +163,8 @@ extension ContentView {
})
.buttonStyle(
ToolbarStatusButtonStyle(
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
lightColor: agentLaunchController.running ? .black.opacity(0.05) : .red.opacity(0.75),
darkColor: agentLaunchController.running ? .white.opacity(0.05) : .red.opacity(0.5),
)
)
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {