@hasna/recordings 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +349 -69
  2. package/bun.lock +3 -0
  3. package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
  4. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
  5. package/dist/cli/index.js +1728 -320
  6. package/dist/cli/macos-permissions.d.ts +13 -0
  7. package/dist/cli/macos-permissions.d.ts.map +1 -0
  8. package/dist/cli/options.d.ts +12 -0
  9. package/dist/cli/options.d.ts.map +1 -1
  10. package/dist/db/pg-migrations.d.ts.map +1 -1
  11. package/dist/db/recordings.d.ts +2 -1
  12. package/dist/db/recordings.d.ts.map +1 -1
  13. package/dist/db/remote-storage.d.ts +5 -1
  14. package/dist/db/remote-storage.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +628 -147
  18. package/dist/lib/bun-runtime.d.ts +11 -0
  19. package/dist/lib/bun-runtime.d.ts.map +1 -0
  20. package/dist/lib/config.d.ts +8 -1
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/enhancer.d.ts +10 -2
  23. package/dist/lib/enhancer.d.ts.map +1 -1
  24. package/dist/lib/install-maintenance.d.ts +24 -0
  25. package/dist/lib/install-maintenance.d.ts.map +1 -0
  26. package/dist/lib/machine.d.ts +2 -0
  27. package/dist/lib/machine.d.ts.map +1 -0
  28. package/dist/lib/recorder.d.ts +2 -1
  29. package/dist/lib/recorder.d.ts.map +1 -1
  30. package/dist/lib/recording-create-identity.d.ts +13 -0
  31. package/dist/lib/recording-create-identity.d.ts.map +1 -0
  32. package/dist/lib/release-install-policy.d.ts +31 -0
  33. package/dist/lib/release-install-policy.d.ts.map +1 -0
  34. package/dist/lib/transcriber.d.ts.map +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +977 -194
  37. package/dist/sdk/index.js +5 -1
  38. package/dist/sdk/v1.generated.d.ts +4 -0
  39. package/dist/sdk/v1.generated.d.ts.map +1 -1
  40. package/dist/server/cloud-config.d.ts +9 -0
  41. package/dist/server/cloud-config.d.ts.map +1 -0
  42. package/dist/server/cloud-readiness.d.ts +10 -0
  43. package/dist/server/cloud-readiness.d.ts.map +1 -0
  44. package/dist/server/cloud.d.ts +9 -18
  45. package/dist/server/cloud.d.ts.map +1 -1
  46. package/dist/server/index.js +1654 -304
  47. package/dist/server/migrate-command.d.ts +11 -0
  48. package/dist/server/migrate-command.d.ts.map +1 -0
  49. package/dist/server/openapi.d.ts +22 -0
  50. package/dist/server/openapi.d.ts.map +1 -1
  51. package/dist/server/repo.d.ts +8 -1
  52. package/dist/server/repo.d.ts.map +1 -1
  53. package/dist/server/serve.d.ts +6 -1
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage.js +515 -80
  57. package/dist/store.d.ts +3 -1
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +10 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/package.json +7 -4
  63. package/packaging/macos/Empty.entitlements +5 -0
  64. package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
  65. package/packaging/macos/Verifier.entitlements +10 -0
  66. package/packaging/macos/artifact-verifier.sb +33 -0
  67. package/packaging/macos/build_release_pkg.sh +872 -0
  68. package/packaging/macos/managed_bootstrap.sh +623 -0
  69. package/packaging/macos/pkgutil_fingerprint.awk +37 -0
  70. package/packaging/macos/release_lifecycle.ts +463 -0
  71. package/packaging/macos/scripts/postinstall +565 -0
  72. package/packaging/macos/scripts/preinstall +297 -0
  73. package/scripts/build_companion_cli.sh +337 -0
  74. package/scripts/build_native_fs_guard.sh +59 -0
  75. package/scripts/generate-sdk.ts +19 -1
  76. package/scripts/install_macos_app.sh +1919 -98
  77. package/scripts/macos_artifact.ts +5316 -0
  78. package/scripts/migrate.ts +38 -10
  79. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  80. package/scripts/native/recordings_fs_guard.c +1167 -0
  81. package/scripts/native_fs_guard.ts +158 -0
  82. package/scripts/release-guard.ts +20 -1
  83. package/scripts/resolve_tailscale_cli.sh +389 -0
  84. package/scripts/smoke_macos_app.sh +573 -0
  85. package/src/native/Recordings/App/ContentView.swift +83 -0
  86. package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
  87. package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
  88. package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
  89. package/src/native/Recordings/App/RecordingsApp.swift +367 -44
  90. package/src/native/Recordings/App/RecordingsListView.swift +130 -0
  91. package/src/native/Recordings/App/RecordingsStore.swift +206 -0
  92. package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
  93. package/src/native/Recordings/App/SidebarView.swift +212 -0
  94. package/src/native/Recordings/App/Theme.swift +87 -0
  95. package/src/native/Recordings/Package.resolved +5 -5
  96. package/src/native/Recordings/Package.swift +62 -2
  97. package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
  98. package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
  99. package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
  101. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
  102. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
  103. package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
  104. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
  105. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
  106. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
  107. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
  108. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
  109. package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
  110. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
  111. package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
  112. package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
  113. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
  114. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
  115. package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
  116. package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
  117. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
  118. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
  119. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
  120. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
  121. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
  122. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
  123. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
  124. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
  125. package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
  126. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
  127. package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
  128. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
  129. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
  130. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
  131. package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
  132. package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
  133. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
  134. package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
  135. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
  136. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
  137. package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
  138. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
  139. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
  140. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
  141. package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
  142. package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
  143. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
  144. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
  145. package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
  146. package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
  147. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
  148. package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
  149. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
  150. package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
  151. package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
  152. package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
  153. package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
  154. package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
  155. package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
  156. package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
  157. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
  158. package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
  159. package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
  160. package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
  161. package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
  162. package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
  163. package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
  164. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
  165. package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
  166. package/src/native/Recordings/build.sh +1973 -236
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
@@ -0,0 +1,119 @@
1
+ import Darwin
2
+ import Foundation
3
+ import RecordingsUpdateProtocol
4
+
5
+ @main
6
+ enum RecordingsUpdateClientMain {
7
+ static func main() {
8
+ do {
9
+ let command = try ClientCommand.parse(CommandLine.arguments)
10
+ let connection = NSXPCConnection(
11
+ machServiceName: RecordingsUpdateConstants.machServiceName,
12
+ options: .privileged
13
+ )
14
+ connection.remoteObjectInterface = makeRecordingsUpdateXPCInterface()
15
+ connection.resume()
16
+ defer { connection.invalidate() }
17
+ let reply: NSDictionary
18
+ switch command {
19
+ case .status:
20
+ reply = try invoke(connection: connection) { service, callback in
21
+ service.queryStatus(withReply: callback)
22
+ }
23
+ case let .install(artifactPath, manifestPath, envelopePath):
24
+ let artifact = try retainedRegularFile(path: artifactPath)
25
+ let manifest = try retainedRegularFile(path: manifestPath)
26
+ let envelope = try retainedRegularFile(path: envelopePath)
27
+ defer {
28
+ try? artifact.close()
29
+ try? manifest.close()
30
+ try? envelope.close()
31
+ }
32
+ reply = try invoke(connection: connection) { service, callback in
33
+ service.install(
34
+ archive: artifact,
35
+ manifest: manifest,
36
+ envelope: envelope,
37
+ withReply: callback
38
+ )
39
+ }
40
+ }
41
+ let data = try JSONSerialization.data(withJSONObject: reply, options: [.sortedKeys])
42
+ FileHandle.standardOutput.write(data + Data([0x0a]))
43
+ guard reply[RecordingsUpdateReplyKey.success] as? Bool == true else { Darwin.exit(1) }
44
+ } catch {
45
+ FileHandle.standardError.write(Data("recordings-update-client: request failed\n".utf8))
46
+ Darwin.exit(1)
47
+ }
48
+ }
49
+
50
+ private static func invoke(
51
+ connection: NSXPCConnection,
52
+ operation: (RecordingsUpdateXPCProtocol, @escaping (NSDictionary) -> Void) -> Void
53
+ ) throws -> NSDictionary {
54
+ let semaphore = DispatchSemaphore(value: 0)
55
+ let lock = NSLock()
56
+ var result: NSDictionary?
57
+ var failed = false
58
+ let proxy = connection.remoteObjectProxyWithErrorHandler { _ in
59
+ lock.lock(); failed = true; lock.unlock()
60
+ semaphore.signal()
61
+ }
62
+ guard let service = proxy as? RecordingsUpdateXPCProtocol else {
63
+ throw ClientError.brokerUnavailable
64
+ }
65
+ operation(service) { reply in
66
+ lock.lock(); result = reply; lock.unlock()
67
+ semaphore.signal()
68
+ }
69
+ guard semaphore.wait(timeout: .now() + 300) == .success else {
70
+ throw ClientError.timeout
71
+ }
72
+ lock.lock(); defer { lock.unlock() }
73
+ guard !failed, let result else { throw ClientError.brokerUnavailable }
74
+ return result
75
+ }
76
+
77
+ private static func retainedRegularFile(path: String) throws -> FileHandle {
78
+ guard path.hasPrefix("/") else { throw ClientError.invalidArguments }
79
+ let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
80
+ guard descriptor >= 0 else { throw ClientError.invalidInput }
81
+ var metadata = stat()
82
+ guard fstat(descriptor, &metadata) == 0,
83
+ (metadata.st_mode & S_IFMT) == S_IFREG,
84
+ metadata.st_size > 0
85
+ else {
86
+ Darwin.close(descriptor)
87
+ throw ClientError.invalidInput
88
+ }
89
+ return FileHandle(fileDescriptor: descriptor, closeOnDealloc: true)
90
+ }
91
+ }
92
+
93
+ private enum ClientCommand {
94
+ case status
95
+ case install(artifact: String, manifest: String, envelope: String)
96
+
97
+ static func parse(_ arguments: [String]) throws -> ClientCommand {
98
+ if arguments.count == 2, arguments[1] == "status" { return .status }
99
+ guard arguments.count == 8,
100
+ arguments[1] == "install" || arguments[1] == "bootstrap",
101
+ arguments[2] == "--artifact",
102
+ arguments[4] == "--manifest",
103
+ arguments[6] == "--envelope",
104
+ arguments[3].hasPrefix("/"),
105
+ arguments[5].hasPrefix("/"),
106
+ arguments[7].hasPrefix("/")
107
+ else {
108
+ throw ClientError.invalidArguments
109
+ }
110
+ return .install(artifact: arguments[3], manifest: arguments[5], envelope: arguments[7])
111
+ }
112
+ }
113
+
114
+ private enum ClientError: Error {
115
+ case invalidArguments
116
+ case invalidInput
117
+ case brokerUnavailable
118
+ case timeout
119
+ }
@@ -0,0 +1,159 @@
1
+ import Darwin
2
+ import Foundation
3
+
4
+ public struct BoundedProcessResult: Sendable {
5
+ public let standardOutput: Data
6
+ public let exitedNormally: Bool
7
+ public let terminationStatus: Int32
8
+ }
9
+
10
+ public enum BoundedProcessError: Error, Equatable, Sendable {
11
+ case invalidConfiguration
12
+ case launchFailed
13
+ case timedOut
14
+ case outputTooLarge
15
+ case outputReadFailed
16
+ }
17
+
18
+ /// Runs a fixed executable with bounded stdout and a hard wall-clock deadline.
19
+ ///
20
+ /// Stdout is drained with nonblocking `poll(2)`, preventing either the child or an
21
+ /// inherited-pipe descendant from extending the wall-clock bound. The reader
22
+ /// retains at most `maximumOutputBytes`; the first byte beyond that limit kills the
23
+ /// direct child, closes the pipe, and fails closed.
24
+ public enum BoundedProcessRunner {
25
+ public static func run(
26
+ executablePath: String,
27
+ arguments: [String],
28
+ environment: [String: String],
29
+ maximumOutputBytes: Int,
30
+ timeout: TimeInterval
31
+ ) throws -> BoundedProcessResult {
32
+ guard executablePath.hasPrefix("/"),
33
+ maximumOutputBytes >= 0,
34
+ timeout > 0,
35
+ timeout <= 3_600,
36
+ timeout.isFinite
37
+ else {
38
+ throw BoundedProcessError.invalidConfiguration
39
+ }
40
+
41
+ let process = Process()
42
+ process.executableURL = URL(fileURLWithPath: executablePath)
43
+ process.arguments = arguments
44
+ process.environment = environment
45
+ process.standardInput = FileHandle.nullDevice
46
+ let output = Pipe()
47
+ process.standardOutput = output
48
+ process.standardError = FileHandle.nullDevice
49
+ do {
50
+ try process.run()
51
+ } catch {
52
+ throw BoundedProcessError.launchFailed
53
+ }
54
+
55
+ let processIdentifier = process.processIdentifier
56
+ let outputHandle = output.fileHandleForReading
57
+ let outputDescriptor = outputHandle.fileDescriptor
58
+ let existingFlags = Darwin.fcntl(outputDescriptor, F_GETFL)
59
+ guard existingFlags >= 0,
60
+ Darwin.fcntl(outputDescriptor, F_SETFL, existingFlags | O_NONBLOCK) >= 0
61
+ else {
62
+ if process.isRunning { _ = Darwin.kill(processIdentifier, SIGKILL) }
63
+ process.waitUntilExit()
64
+ throw BoundedProcessError.outputReadFailed
65
+ }
66
+ let startedAt = DispatchTime.now().uptimeNanoseconds
67
+ let timeoutNanoseconds = UInt64(timeout * 1_000_000_000)
68
+ let deadline = startedAt + timeoutNanoseconds
69
+
70
+ var standardOutput = Data()
71
+ var outputTooLarge = false
72
+ var outputReadFailed = false
73
+ var timedOut = false
74
+ var reachedEOF = false
75
+ var buffer = [UInt8](repeating: 0, count: 4_096)
76
+ while !reachedEOF && !outputTooLarge && !outputReadFailed {
77
+ let now = DispatchTime.now().uptimeNanoseconds
78
+ if now >= deadline {
79
+ timedOut = true
80
+ break
81
+ }
82
+ let remainingNanoseconds = deadline - now
83
+ let remainingMilliseconds = max(
84
+ 1,
85
+ min(
86
+ Int(Int32.max),
87
+ Int((remainingNanoseconds + 999_999) / 1_000_000)
88
+ )
89
+ )
90
+ var descriptor = pollfd(
91
+ fd: outputDescriptor,
92
+ events: Int16(POLLIN | POLLHUP | POLLERR),
93
+ revents: 0
94
+ )
95
+ let pollResult = Darwin.poll(&descriptor, 1, Int32(remainingMilliseconds))
96
+ if pollResult == 0 {
97
+ timedOut = true
98
+ break
99
+ }
100
+ if pollResult < 0 {
101
+ if errno == EINTR { continue }
102
+ outputReadFailed = true
103
+ break
104
+ }
105
+ while true {
106
+ let count = buffer.withUnsafeMutableBytes { bytes in
107
+ Darwin.read(outputDescriptor, bytes.baseAddress, bytes.count)
108
+ }
109
+ if count > 0 {
110
+ if count > maximumOutputBytes - standardOutput.count {
111
+ outputTooLarge = true
112
+ break
113
+ }
114
+ standardOutput.append(contentsOf: buffer.prefix(count))
115
+ continue
116
+ }
117
+ if count == 0 {
118
+ reachedEOF = true
119
+ break
120
+ }
121
+ if errno == EINTR { continue }
122
+ if errno == EAGAIN || errno == EWOULDBLOCK { break }
123
+ outputReadFailed = true
124
+ break
125
+ }
126
+ }
127
+
128
+ while !timedOut && !outputTooLarge && !outputReadFailed && process.isRunning {
129
+ let now = DispatchTime.now().uptimeNanoseconds
130
+ if now >= deadline {
131
+ timedOut = true
132
+ break
133
+ }
134
+ let remaining = Double(deadline - now) / 1_000_000_000
135
+ Thread.sleep(forTimeInterval: min(0.005, remaining))
136
+ }
137
+
138
+ if timedOut || outputTooLarge || outputReadFailed {
139
+ if process.isRunning {
140
+ _ = Darwin.kill(processIdentifier, SIGKILL)
141
+ }
142
+ do {
143
+ try outputHandle.close()
144
+ } catch {
145
+ outputReadFailed = true
146
+ }
147
+ }
148
+ process.waitUntilExit()
149
+
150
+ if timedOut { throw BoundedProcessError.timedOut }
151
+ if outputTooLarge { throw BoundedProcessError.outputTooLarge }
152
+ if outputReadFailed { throw BoundedProcessError.outputReadFailed }
153
+ return BoundedProcessResult(
154
+ standardOutput: standardOutput,
155
+ exitedNormally: process.terminationReason == .exit,
156
+ terminationStatus: process.terminationStatus
157
+ )
158
+ }
159
+ }
@@ -0,0 +1,214 @@
1
+ import Foundation
2
+
3
+ public struct CandidateReleaseMetadataExpectation: Equatable, Sendable {
4
+ public let applicationIdentifier: String
5
+ public let applicationExecutable: String
6
+ public let version: String
7
+ public let build: String
8
+ public let sourceCommit: String
9
+ public let signingTeamIdentifier: String
10
+ public let minimumOSVersion: String
11
+ public let architectures: [String]
12
+
13
+ public init(
14
+ applicationIdentifier: String,
15
+ applicationExecutable: String,
16
+ version: String,
17
+ build: String,
18
+ sourceCommit: String,
19
+ signingTeamIdentifier: String,
20
+ minimumOSVersion: String,
21
+ architectures: [String]
22
+ ) {
23
+ self.applicationIdentifier = applicationIdentifier
24
+ self.applicationExecutable = applicationExecutable
25
+ self.version = version
26
+ self.build = build
27
+ self.sourceCommit = sourceCommit
28
+ self.signingTeamIdentifier = signingTeamIdentifier
29
+ self.minimumOSVersion = minimumOSVersion
30
+ self.architectures = architectures
31
+ }
32
+ }
33
+
34
+ public struct CandidateApplicationMetadata: Equatable, Sendable {
35
+ public let bundleIdentifier: String
36
+ public let shortVersion: String
37
+ public let buildVersion: String
38
+ public let executable: String
39
+ public let minimumOSVersion: String
40
+ public let architectures: [String]
41
+
42
+ public init(
43
+ bundleIdentifier: String,
44
+ shortVersion: String,
45
+ buildVersion: String,
46
+ executable: String,
47
+ minimumOSVersion: String,
48
+ architectures: [String]
49
+ ) {
50
+ self.bundleIdentifier = bundleIdentifier
51
+ self.shortVersion = shortVersion
52
+ self.buildVersion = buildVersion
53
+ self.executable = executable
54
+ self.minimumOSVersion = minimumOSVersion
55
+ self.architectures = architectures
56
+ }
57
+ }
58
+
59
+ public struct CandidateCompanionMetadata: Equatable, Sendable {
60
+ public let sha256: String
61
+ public let architectures: [String]
62
+
63
+ public init(sha256: String, architectures: [String]) {
64
+ self.sha256 = sha256
65
+ self.architectures = architectures
66
+ }
67
+ }
68
+
69
+ public struct CandidateBuildProvenanceMetadata: Equatable, Sendable {
70
+ public let schemaVersion: Int
71
+ public let containsLocalOnlyFields: Bool
72
+ public let bundleIdentifier: String
73
+ public let bundleVersion: String
74
+ public let bundleBuildVersion: String
75
+ public let sourceCommit: String
76
+ public let teamIdentifier: String
77
+ public let minimumMacOS: String
78
+ public let architectures: [String]
79
+ public let companionVersion: String
80
+ public let companionSHA256: String
81
+ public let companionArchitectures: [String]
82
+
83
+ public init(
84
+ schemaVersion: Int,
85
+ containsLocalOnlyFields: Bool,
86
+ bundleIdentifier: String,
87
+ bundleVersion: String,
88
+ bundleBuildVersion: String,
89
+ sourceCommit: String,
90
+ teamIdentifier: String,
91
+ minimumMacOS: String,
92
+ architectures: [String],
93
+ companionVersion: String,
94
+ companionSHA256: String,
95
+ companionArchitectures: [String]
96
+ ) {
97
+ self.schemaVersion = schemaVersion
98
+ self.containsLocalOnlyFields = containsLocalOnlyFields
99
+ self.bundleIdentifier = bundleIdentifier
100
+ self.bundleVersion = bundleVersion
101
+ self.bundleBuildVersion = bundleBuildVersion
102
+ self.sourceCommit = sourceCommit
103
+ self.teamIdentifier = teamIdentifier
104
+ self.minimumMacOS = minimumMacOS
105
+ self.architectures = architectures
106
+ self.companionVersion = companionVersion
107
+ self.companionSHA256 = companionSHA256
108
+ self.companionArchitectures = companionArchitectures
109
+ }
110
+ }
111
+
112
+ public enum CandidateMetadataPolicyError: Error, Equatable, Sendable {
113
+ case applicationIdentifierMismatch
114
+ case applicationVersionMismatch
115
+ case applicationBuildMismatch
116
+ case applicationExecutableMismatch
117
+ case applicationMinimumOSMismatch
118
+ case applicationArchitectureMismatch
119
+ case updateClientArchitectureMismatch
120
+ case companionDigestMismatch
121
+ case companionArchitectureMismatch
122
+ case provenanceSchemaMismatch
123
+ case localOnlyProvenanceRejected
124
+ case provenanceIdentifierMismatch
125
+ case provenanceVersionMismatch
126
+ case provenanceBuildMismatch
127
+ case provenanceSourceMismatch
128
+ case provenanceTeamMismatch
129
+ case provenanceMinimumOSMismatch
130
+ case provenanceArchitectureMismatch
131
+ case provenanceCompanionVersionMismatch
132
+ }
133
+
134
+ /// Pure equality policy between candidate metadata and a verified release envelope.
135
+ ///
136
+ /// Parsing, ownership, and file-stability checks remain at the broker I/O boundary.
137
+ /// This policy runs before the broker enters monotonic state handling, so every
138
+ /// semantic value used to identify a release must exactly equal signed evidence.
139
+ public enum CandidateMetadataPolicy {
140
+ public static func validate(
141
+ application: CandidateApplicationMetadata,
142
+ updateClientArchitectures: [String],
143
+ companion: CandidateCompanionMetadata,
144
+ provenance: CandidateBuildProvenanceMetadata,
145
+ expected: CandidateReleaseMetadataExpectation
146
+ ) throws {
147
+ guard application.bundleIdentifier == expected.applicationIdentifier else {
148
+ throw CandidateMetadataPolicyError.applicationIdentifierMismatch
149
+ }
150
+ guard application.shortVersion == expected.version else {
151
+ throw CandidateMetadataPolicyError.applicationVersionMismatch
152
+ }
153
+ guard application.buildVersion == expected.build else {
154
+ throw CandidateMetadataPolicyError.applicationBuildMismatch
155
+ }
156
+ guard application.executable == expected.applicationExecutable else {
157
+ throw CandidateMetadataPolicyError.applicationExecutableMismatch
158
+ }
159
+ guard application.minimumOSVersion == expected.minimumOSVersion else {
160
+ throw CandidateMetadataPolicyError.applicationMinimumOSMismatch
161
+ }
162
+ guard application.architectures == expected.architectures else {
163
+ throw CandidateMetadataPolicyError.applicationArchitectureMismatch
164
+ }
165
+ guard updateClientArchitectures == expected.architectures else {
166
+ throw CandidateMetadataPolicyError.updateClientArchitectureMismatch
167
+ }
168
+ guard companion.architectures == expected.architectures else {
169
+ throw CandidateMetadataPolicyError.companionArchitectureMismatch
170
+ }
171
+ guard companion.sha256 == provenance.companionSHA256 else {
172
+ throw CandidateMetadataPolicyError.companionDigestMismatch
173
+ }
174
+
175
+ guard provenance.schemaVersion == 4 else {
176
+ throw CandidateMetadataPolicyError.provenanceSchemaMismatch
177
+ }
178
+ guard !provenance.containsLocalOnlyFields else {
179
+ throw CandidateMetadataPolicyError.localOnlyProvenanceRejected
180
+ }
181
+ guard provenance.bundleIdentifier == expected.applicationIdentifier else {
182
+ throw CandidateMetadataPolicyError.provenanceIdentifierMismatch
183
+ }
184
+ guard provenance.bundleVersion == expected.version else {
185
+ throw CandidateMetadataPolicyError.provenanceVersionMismatch
186
+ }
187
+ guard provenance.bundleBuildVersion == expected.build else {
188
+ throw CandidateMetadataPolicyError.provenanceBuildMismatch
189
+ }
190
+ guard provenance.sourceCommit == expected.sourceCommit else {
191
+ throw CandidateMetadataPolicyError.provenanceSourceMismatch
192
+ }
193
+ guard provenance.teamIdentifier == expected.signingTeamIdentifier else {
194
+ throw CandidateMetadataPolicyError.provenanceTeamMismatch
195
+ }
196
+ guard provenance.minimumMacOS == expected.minimumOSVersion else {
197
+ throw CandidateMetadataPolicyError.provenanceMinimumOSMismatch
198
+ }
199
+ // The release build contract requires the packaged companion's --version
200
+ // to equal the app release version before signing. At update time the
201
+ // broker binds that signed semantic assertion to the measured companion
202
+ // digest without executing candidate code as root.
203
+ guard provenance.companionVersion == provenance.bundleVersion,
204
+ provenance.companionVersion == expected.version
205
+ else {
206
+ throw CandidateMetadataPolicyError.provenanceCompanionVersionMismatch
207
+ }
208
+ guard provenance.architectures == expected.architectures,
209
+ provenance.companionArchitectures == expected.architectures
210
+ else {
211
+ throw CandidateMetadataPolicyError.provenanceArchitectureMismatch
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,55 @@
1
+ public enum HostOSVersionPolicyError: Error, Equatable, Sendable {
2
+ case hostProductVersionUnavailable
3
+ case malformedCandidateMinimumOSVersion
4
+ case malformedHostProductVersion
5
+ case candidateRequiresNewerOS
6
+ }
7
+
8
+ /// Pure compatibility policy for signed minimum-OS evidence and live host evidence.
9
+ ///
10
+ /// macOS ProductVersion and LSMinimumSystemVersion commonly omit a zero patch
11
+ /// component. Parse two- and three-component numeric versions, normalize the
12
+ /// missing patch to zero, and never fall back to lexical comparison.
13
+ public enum HostOSVersionPolicy {
14
+ public static func validate(
15
+ candidateMinimumOSVersion: String,
16
+ hostProductVersion: String?
17
+ ) throws {
18
+ guard let candidate = components(candidateMinimumOSVersion) else {
19
+ throw HostOSVersionPolicyError.malformedCandidateMinimumOSVersion
20
+ }
21
+ guard let hostProductVersion else {
22
+ throw HostOSVersionPolicyError.hostProductVersionUnavailable
23
+ }
24
+ guard let host = components(hostProductVersion) else {
25
+ throw HostOSVersionPolicyError.malformedHostProductVersion
26
+ }
27
+ guard !host.lexicographicallyPrecedes(candidate) else {
28
+ throw HostOSVersionPolicyError.candidateRequiresNewerOS
29
+ }
30
+ }
31
+
32
+ public static func isValidNumericVersion(_ value: String) -> Bool {
33
+ components(value) != nil
34
+ }
35
+
36
+ private static func components(_ value: String) -> [UInt64]? {
37
+ guard !value.isEmpty, value.utf8.count <= 64 else { return nil }
38
+ let fields = value.split(separator: ".", omittingEmptySubsequences: false)
39
+ guard fields.count == 2 || fields.count == 3 else { return nil }
40
+ var result: [UInt64] = []
41
+ result.reserveCapacity(3)
42
+ for field in fields {
43
+ guard !field.isEmpty,
44
+ field.utf8.allSatisfy({ $0 >= 0x30 && $0 <= 0x39 }),
45
+ (field.count == 1 || field.first != "0"),
46
+ let component = UInt64(field)
47
+ else {
48
+ return nil
49
+ }
50
+ result.append(component)
51
+ }
52
+ if result.count == 2 { result.append(0) }
53
+ return result
54
+ }
55
+ }
@@ -0,0 +1,152 @@
1
+ import Foundation
2
+
3
+ public enum MonotonicReleasePhase: String, Equatable, Sendable {
4
+ case seen
5
+ case aborted
6
+ case committed
7
+ }
8
+
9
+ public struct MonotonicReleaseCandidate: Equatable, Sendable {
10
+ public let purpose: String
11
+ public let keyEpoch: UInt64
12
+ public let releaseSequence: UInt64
13
+ public let releaseID: String
14
+ public let cohortPackageSHA256: String
15
+ public let envelopePayloadSHA256: String
16
+
17
+ public init(
18
+ purpose: String,
19
+ keyEpoch: UInt64,
20
+ releaseSequence: UInt64,
21
+ releaseID: String,
22
+ cohortPackageSHA256: String,
23
+ envelopePayloadSHA256: String
24
+ ) {
25
+ self.purpose = purpose
26
+ self.keyEpoch = keyEpoch
27
+ self.releaseSequence = releaseSequence
28
+ self.releaseID = releaseID
29
+ self.cohortPackageSHA256 = cohortPackageSHA256
30
+ self.envelopePayloadSHA256 = envelopePayloadSHA256
31
+ }
32
+ }
33
+
34
+ public struct MonotonicReleaseStateSnapshot: Equatable, Sendable {
35
+ public let purpose: String
36
+ public let phase: MonotonicReleasePhase
37
+ public let keyEpoch: UInt64
38
+ public let releaseSequence: UInt64
39
+ public let releaseID: String
40
+ public let cohortPackageSHA256: String
41
+ public let envelopePayloadSHA256: String
42
+
43
+ public init(
44
+ purpose: String,
45
+ phase: MonotonicReleasePhase,
46
+ keyEpoch: UInt64,
47
+ releaseSequence: UInt64,
48
+ releaseID: String,
49
+ cohortPackageSHA256: String,
50
+ envelopePayloadSHA256: String
51
+ ) {
52
+ self.purpose = purpose
53
+ self.phase = phase
54
+ self.keyEpoch = keyEpoch
55
+ self.releaseSequence = releaseSequence
56
+ self.releaseID = releaseID
57
+ self.cohortPackageSHA256 = cohortPackageSHA256
58
+ self.envelopePayloadSHA256 = envelopePayloadSHA256
59
+ }
60
+ }
61
+
62
+ public enum MonotonicReleasePolicyDecision: Equatable, Sendable {
63
+ case advance
64
+ case resumeSeen
65
+ case alreadyCommitted
66
+ }
67
+
68
+ public enum MonotonicReleasePolicyError: Error, Equatable, Sendable {
69
+ case immutableCohortChange
70
+ case bootstrapRequired
71
+ case bootstrapAlreadyInitialized
72
+ case sequenceRollbackOrConflict
73
+ case pendingSeenRecoveryRequired
74
+ }
75
+
76
+ /// Pure transition policy for the durable updater state machine.
77
+ ///
78
+ /// A `seen` record is a recovery barrier: until its exact release commits or is
79
+ /// durably aborted after exact-tree rollback, no
80
+ /// other release may replace it, even when the other release has a higher
81
+ /// sequence. An `aborted` record preserves the highest observed sequence while
82
+ /// allowing only a strictly newer release from the same immutable cohort.
83
+ public enum MonotonicReleasePolicy {
84
+ public static func assess(
85
+ candidate: MonotonicReleaseCandidate,
86
+ current: MonotonicReleaseStateSnapshot?,
87
+ initialKeyEpoch: UInt64,
88
+ allowedKeyEpochs: [UInt64]
89
+ ) throws -> MonotonicReleasePolicyDecision {
90
+ guard allowedKeyEpochs == [initialKeyEpoch],
91
+ candidate.keyEpoch == initialKeyEpoch
92
+ else {
93
+ throw MonotonicReleasePolicyError.immutableCohortChange
94
+ }
95
+
96
+ guard let current else {
97
+ guard candidate.purpose == "bootstrap" else {
98
+ throw MonotonicReleasePolicyError.bootstrapRequired
99
+ }
100
+ return .advance
101
+ }
102
+
103
+ guard current.keyEpoch == initialKeyEpoch else {
104
+ throw MonotonicReleasePolicyError.immutableCohortChange
105
+ }
106
+
107
+ let isExactReplay = candidate.purpose == current.purpose
108
+ && candidate.keyEpoch == current.keyEpoch
109
+ && candidate.releaseSequence == current.releaseSequence
110
+ && candidate.releaseID == current.releaseID
111
+ && candidate.cohortPackageSHA256 == current.cohortPackageSHA256
112
+ && candidate.envelopePayloadSHA256 == current.envelopePayloadSHA256
113
+
114
+ if current.phase == .seen {
115
+ guard isExactReplay else {
116
+ throw MonotonicReleasePolicyError.pendingSeenRecoveryRequired
117
+ }
118
+ return .resumeSeen
119
+ }
120
+
121
+ if current.phase == .aborted {
122
+ guard candidate.purpose != "bootstrap" else {
123
+ throw MonotonicReleasePolicyError.bootstrapAlreadyInitialized
124
+ }
125
+ guard candidate.cohortPackageSHA256 == current.cohortPackageSHA256 else {
126
+ throw MonotonicReleasePolicyError.immutableCohortChange
127
+ }
128
+ guard candidate.releaseSequence > current.releaseSequence else {
129
+ throw MonotonicReleasePolicyError.sequenceRollbackOrConflict
130
+ }
131
+ return .advance
132
+ }
133
+
134
+ if candidate.purpose == "bootstrap" {
135
+ guard current.purpose == "bootstrap", isExactReplay else {
136
+ throw MonotonicReleasePolicyError.bootstrapAlreadyInitialized
137
+ }
138
+ return .alreadyCommitted
139
+ }
140
+
141
+ guard candidate.cohortPackageSHA256 == current.cohortPackageSHA256 else {
142
+ throw MonotonicReleasePolicyError.immutableCohortChange
143
+ }
144
+ if isExactReplay {
145
+ return .alreadyCommitted
146
+ }
147
+ guard candidate.releaseSequence > current.releaseSequence else {
148
+ throw MonotonicReleasePolicyError.sequenceRollbackOrConflict
149
+ }
150
+ return .advance
151
+ }
152
+ }