This commit is contained in:
Max Goedjen
2026-09-13 23:15:09 -07:00
parent 11b6adb648
commit f45aa6b6eb
9 changed files with 284 additions and 104 deletions
@@ -365,16 +365,6 @@
}, },
"shouldTranslate" : false "shouldTranslate" : false
}, },
"%@ - %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ - %2$@"
}
}
}
},
"about_build_log_button" : { "about_build_log_button" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -6130,6 +6120,19 @@
}, },
"Certificates" : { "Certificates" : {
},
"Connecting to %@@%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Connecting to %1$@@%2$@"
}
}
}
},
"Connecting to unknown host" : {
}, },
"copyable_click_to_copy_button" : { "copyable_click_to_copy_button" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -20463,7 +20466,7 @@
"Review" : { "Review" : {
}, },
"Review All" : { "Review as Batch" : {
}, },
"secret_detail_certificate_path_label" : { "secret_detail_certificate_path_label" : {
@@ -24920,6 +24923,9 @@
} }
} }
} }
},
"Signing for %@" : {
}, },
"smart_card" : { "smart_card" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -0,0 +1,94 @@
import SwiftUI
public struct PrimaryButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
@Environment(\.isEnabled) var isEnabled
public func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark, isEnabled {
content.buttonStyle(.glassProminent)
} else {
content.buttonStyle(.borderedProminent)
}
}
}
extension View {
public func primaryButton() -> some View {
modifier(PrimaryButtonModifier())
}
}
public struct ToolbarCircleButtonModifier: ViewModifier {
public func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content
.glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle)
} else {
content
.buttonStyle(.borderless)
}
}
}
extension View {
public func toolbarCircleButton() -> some View {
modifier(ToolbarCircleButtonModifier())
}
}
public struct NormalButtonModifier: ViewModifier {
public func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.buttonStyle(.glass)
} else {
content.buttonStyle(.bordered)
}
}
}
extension View {
public func normalButton() -> some View {
modifier(NormalButtonModifier())
}
}
public struct DangerButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
public func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark {
content.buttonStyle(.glassProminent)
.tint(.red)
.foregroundStyle(.white)
} else {
content.buttonStyle(.borderedProminent)
.tint(.red)
.foregroundStyle(.white)
}
}
}
extension View {
public func danger() -> some View {
modifier(DangerButtonModifier())
}
}
@@ -97,6 +97,12 @@ public struct FixedTitleView: View {
let subtitle: LocalizedStringResource? let subtitle: LocalizedStringResource?
let image: Image let image: Image
public init(title: LocalizedStringResource, subtitle: LocalizedStringResource?, image: Image) {
self.title = title
self.subtitle = subtitle
self.image = image
}
public var body: some View { public var body: some View {
HStack { HStack {
image image
@@ -185,7 +185,7 @@ extension Agent {
} }
func signWithRequiredAuthentication(data: Data, store: AnySecretStore, secret: AnySecret, provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data { func signWithRequiredAuthentication(data: Data, store: AnySecretStore, secret: AnySecret, provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data {
let context = try await authenticationHandler.waitForAuthentication(for: SignatureRequest(secret: secret, provenance: provenance)) let context = try await authenticationHandler.waitForAuthentication(for: SignatureRequest(secret: secret, provenance: provenance, target: target))
let result = try await store.sign(data: data, with: secret, for: provenance, target: target, context: context.laContext) let result = try await store.sign(data: data, with: secret, for: provenance, target: target, context: context.laContext)
let signedData = signatureWriter.data(secret: secret, signature: result) let signedData = signatureWriter.data(secret: secret, signature: result)
try await witness?.witness(accessTo: secret, from: store, by: provenance, target: target, offerPersistence: false) // FIXME: THIS try await witness?.witness(accessTo: secret, from: store, by: provenance, target: target, offerPersistence: false) // FIXME: THIS
@@ -69,7 +69,7 @@ public final class AuthenticationContext: AuthenticationContextProtocol {
private var persistedContexts: [AnySecret: AuthenticationContext] = [:] private var persistedContexts: [AnySecret: AuthenticationContext] = [:]
private var holdingRequests: Set<SignatureRequest> = [] private var holdingRequests: Set<SignatureRequest> = []
private var activeTask: Task<Void, any Error>? private var activeTask: Task<Bool, any Error>?
private var lastBatchAuthPresentation: Set<SignatureRequest>? private var lastBatchAuthPresentation: Set<SignatureRequest>?
private var presentBatchAuth: (() async throws -> Void)? private var presentBatchAuth: (() async throws -> Void)?
@@ -118,10 +118,15 @@ public final class AuthenticationContext: AuthenticationContextProtocol {
activeTask = Task { activeTask = Task {
logger.log("Beginning individual auth prompt") logger.log("Beginning individual auth prompt")
_ = try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason) let result = (try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason)) ?? false
logger.log("Ended individual auth prompt") logger.log("Ended individual auth prompt")
return result
}
let result = try? await activeTask?.value
if result == false {
holdingRequests.remove(request)
return context
} }
_ = try? await activeTask?.value
// TODO: Check something beyond cancellation? id? // TODO: Check something beyond cancellation? id?
// Is this okay? Do we always assume that a cancelled task will be the proceeded on? // Is this okay? Do we always assume that a cancelled task will be the proceeded on?
if activeTask?.isCancelled ?? false { if activeTask?.isCancelled ?? false {
@@ -19,17 +19,22 @@ public struct SignatureRequest: Identifiable, Hashable, Sendable, Comparable {
public let date: Date public let date: Date
public let secret: AnySecret public let secret: AnySecret
public let provenance: SigningRequestProvenance public let provenance: SigningRequestProvenance
public let target: SigningRequestTarget?
public init(secret: AnySecret, provenance: SigningRequestProvenance) { public init(secret: AnySecret, provenance: SigningRequestProvenance, target: SigningRequestTarget?) {
self.id = UUID() self.id = UUID()
self.date = Date() self.date = Date()
self.secret = secret self.secret = secret
self.provenance = provenance self.provenance = provenance
self.target = target
} }
public var batchID: Int { public var batchID: Int {
var hasher = Hasher() var hasher = Hasher()
provenance.batchID.hash(into: &hasher) provenance.batchID.hash(into: &hasher)
if let target {
target.batchID.hash(into: &hasher)
}
secret.id.hash(into: &hasher) secret.id.hash(into: &hasher)
return hasher.finalize() return hasher.finalize()
} }
@@ -2,12 +2,17 @@ import Foundation
import AppKit import AppKit
/// Describes the target of the signature operation. /// Describes the target of the signature operation.
public enum SigningRequestTarget: Sendable { public enum SigningRequestTarget: Sendable, Hashable {
case connection(ConnectionPayload) case connection(ConnectionPayload)
case signature(SignaturePayload) case signature(SignaturePayload)
public struct ConnectionPayload: Sendable, Codable{ public var batchID: Int {
hashValue
}
public struct ConnectionPayload: Sendable, Codable, Hashable {
public let username: String public let username: String
public let hasSignature: Bool public let hasSignature: Bool
@@ -34,7 +39,7 @@ public enum SigningRequestTarget: Sendable {
} }
public struct SignaturePayload: Sendable, Codable { public struct SignaturePayload: Sendable, Codable, Hashable {
public let namespace: String public let namespace: String
public let hashAlgorithm: String public let hashAlgorithm: String
+3 -21
View File
@@ -66,25 +66,6 @@ struct SecretAgent: App {
} }
} }
} }
// .task {
// let socketController = SocketController(path: URL.agentHomeURL.appendingPathComponent("socket-two.ssh").path())
// let socketController = SocketController(path: "/Users/max/Downloads/test.ssh")
// let agent = Agent(storeList: storeList, authenticationHandler: authenticationHandler, witness: notifier)
// for await session in socketController.sessions {
// Task {
// let inputParser = try await XPCAgentInputParser()
// do {
// 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 session.write(agentResponse)
// }
// } catch {
// try session.close()
// }
// }
// }
// }
.task { .task {
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) { for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
@@ -101,7 +82,7 @@ struct SecretAgent: App {
} }
.task { .task {
authenticationHandler.setBatchAuthHandler { @MainActor in authenticationHandler.setBatchAuthHandler { @MainActor in
openWindow(id: String(describing: BatchedRequestsView.self)) openWindow(value: BatchedRequestsViewIdentifier())
} }
} }
@@ -119,7 +100,7 @@ struct SecretAgent: App {
} }
} }
} }
WindowGroup(id: String(describing: BatchedRequestsView.self)) { WindowGroup(for: BatchedRequestsViewIdentifier.self) { _ in
pendingView pendingView
} }
.windowStyle(.hiddenTitleBar) .windowStyle(.hiddenTitleBar)
@@ -135,3 +116,4 @@ struct SecretAgent: App {
} }
struct BatchedRequestsViewIdentifier: Codable, Hashable {}
+141 -64
View File
@@ -13,44 +13,70 @@ struct BatchedRequestsView: View {
} }
var body: some View { var body: some View {
VStack(alignment: .leading) { ScrollView {
Form { Text("Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually.")
Text("Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually.") ForEach(Array(authenticationHandler.batchableRequests.enumerated()), id: \.offset) { group in
ForEach(Array(authenticationHandler.batchableRequests.enumerated()), id: \.offset) { group in MultilineInfoView {
Section { if let first = group.element.first {
ForEach(Array(group.element.enumerated()), id: \.offset) { pending in HStack {
HStack { HStack {
Image(nsImage: .init(byReferencing: first.provenance.origin.iconURL!))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(pending.element.provenance.origin.displayName) Text(first.provenance.origin.displayName)
.font(.subheadline)
Text(first.secret.name)
.font(.headline) .font(.headline)
Text(pending.element.provenance.date.formatted()) switch first.target {
.font(.footnote) case .connection(let payload):
} if let host = payload.host {
Spacer() Text("Connecting to \(payload.username)@\(host)")
Button("Review") { .font(.caption2)
Task { } else {
try? await authenticationHandler.requestAuthentication(for: [pending.element]) Text("Connecting to unknown host")
.font(.caption2)
}
case .signature(let payload):
Text("Signing for \(payload.namespace)")
.font(.caption2)
default:
EmptyView()
} }
} }
} }
}
} header: {
HStack {
Text("\(group.element.first!.provenance.origin.displayName) - \(group.element.first!.secret.name)")
Spacer() Spacer()
Button("Review All") { VStack {
Task { Button("Review as Batch") {
try? await authenticationHandler.requestAuthentication(for: Set(group.element)) Task {
try? await authenticationHandler.requestAuthentication(for: Set(group.element))
}
} }
.buttonBorderShape(.capsule)
.primaryButton()
} }
} }
} }
} items: {
ForEach(Array(group.element.enumerated()), id: \.offset) { pending in
HStack {
Text(pending.element.provenance.date.formatted())
Spacer()
Button("Review") {
Task {
try? await authenticationHandler.requestAuthentication(for: [pending.element])
}
}
.buttonBorderShape(.capsule)
.normalButton()
}
}
} }
} }
.formStyle(.grouped)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .safeAreaPadding(20)
} }
} }
@@ -76,43 +102,94 @@ private struct TestHandler: AuthenticationHandlerProtocol {
} }
} }
//
//#Preview {
// ScrollView { #Preview {
// MultilineInfoView(title: "GitHub", subtitle: "Ghostty", image: Image(systemName: "lock"), items: [ if #available(macOS 26.0, *) {
// " ScrollView {
// ]) MultilineInfoView {
//// Section { HStack {
//// ForEach(0..<2) { _ in HStack {
//// VStack(alignment: .leading) { Image("ghostty")
//// Text("Ghostty") .resizable()
//// .font(.headline) .aspectRatio(contentMode: .fit)
//// Text("zsh 􀯻 git 􀯻 zsh") .frame(width: 50)
//// .font(.footnote) VStack(alignment: .leading) {
//// Text("4:05 PM") Text("Ghostty")
//// } .font(.subheadline)
//// } Text("GitHub")
//// } header: { .font(.headline)
//// Text("GitHub") Text("Authenticating git@github.com")
//// } .font(.caption2)
//// Section { }
//// ForEach(0..<2) { _ in }
//// VStack(alignment: .leading) { Spacer()
//// Text("Ghostty") VStack {
//// .font(.headline) Button("Review as Batch") {
//// Text("zsh 􀯻 git")
//// .font(.footnote.monospaced()) }
//// Text("Git Signature") .buttonBorderShape(.capsule)
//// .font(.footnote) .buttonStyle(.glassProminent)
//// Text("4:05 PM") }
//// .font(.caption) }
//// } } items: {
//// } ForEach(0..<2) { _ in
//// } header: { HStack {
//// Text("GitHub Signing Key") Text("4:05 PM")
//// } Spacer()
// } Button("Review") {
// .padding()
// .formStyle(.grouped) }
// .frame(minHeight: 700) .buttonBorderShape(.capsule)
//} .buttonStyle(.glass)
}
}
}
MultilineInfoView {
HStack {
HStack {
Image("ghostty")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50)
VStack(alignment: .leading) {
Text("Ghostty")
.font(.subheadline)
Text("Git Signing")
.font(.headline)
Text("Git Signature")
.font(.caption2)
}
}
Spacer()
VStack {
Button("Review as Batch") {
}
.buttonBorderShape(.capsule)
.buttonStyle(.glassProminent)
}
}
} items: {
ForEach(0..<2) { _ in
HStack {
Text("4:05 PM")
Spacer()
Button("Review") {
}
.buttonBorderShape(.capsule)
.buttonStyle(.glass)
}
}
}
}
.padding()
.formStyle(.grouped)
.frame(minHeight: 700)
}
}