@hasna/recordings 0.2.10 → 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 +350 -35
  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 +1922 -77
  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 +1990 -29
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
@@ -0,0 +1,477 @@
1
+ import CryptoKit
2
+ import Darwin
3
+ import Foundation
4
+ import RecordingsUpdateProtocol
5
+
6
+ struct AtomicActivator {
7
+ /// Persist the exact activation binding before MonotonicReleaseStateStore writes
8
+ /// its highest-seen barrier. A crash can therefore never leave an unrecoverable
9
+ /// `seen` state whose signed envelope must be replayed after expiry.
10
+ func prepareActivation(
11
+ candidatePath: String,
12
+ stagedUpdate: StagedUpdate,
13
+ payload: ReleaseEnvelopePayload,
14
+ excludingAuthenticatedClientPID: pid_t
15
+ ) throws {
16
+ try validateApplicationsDirectory()
17
+ do {
18
+ try ApplicationProcessQuiescence.requireQuiescence(
19
+ excludingAuthenticatedClientPID: excludingAuthenticatedClientPID
20
+ )
21
+ } catch {
22
+ throw AtomicActivationError.activationDeferred
23
+ }
24
+
25
+ let applicationPath = RecordingsUpdateConstants.applicationPath
26
+ let previousPath = stagedUpdate.directory + "/previous.app"
27
+ let failedCandidatePath = stagedUpdate.directory + "/failed-candidate.app"
28
+ let previousDigest = try digestIfPresent(applicationPath)
29
+ let candidateMode = try applicationMode(candidatePath)
30
+ let previousMode = previousDigest == nil ? nil : try applicationMode(applicationPath)
31
+ let candidateExecutableModes = try applicationExecutableModes(candidatePath)
32
+ let previousExecutableModes = previousDigest == nil
33
+ ? nil
34
+ : try applicationExecutableModes(applicationPath)
35
+ guard try digestIfPresent(candidatePath) == payload.candidateTreeSHA256,
36
+ try digestIfPresent(previousPath) == nil,
37
+ try digestIfPresent(failedCandidatePath) == nil
38
+ else {
39
+ throw AtomicActivationError.invalidPreparedState
40
+ }
41
+ let envelopePayloadDigest = try SHA256.hash(data: payload.canonicalData())
42
+ .map { String(format: "%02x", $0) }
43
+ .joined()
44
+ let journal = BrokerInstallJournal(
45
+ schemaVersion: RecordingsUpdateConstants.protocolVersion,
46
+ transactionID: stagedUpdate.transactionID.uuidString.lowercased(),
47
+ phase: "prepared",
48
+ releaseID: payload.releaseID,
49
+ releaseSequence: payload.releaseSequence,
50
+ keyEpoch: payload.keyEpoch,
51
+ envelopePayloadSHA256: envelopePayloadDigest,
52
+ artifactSHA256: stagedUpdate.archive.sha256,
53
+ manifestSHA256: stagedUpdate.manifest.sha256,
54
+ cohortPackageSHA256: payload.packageSHA256,
55
+ candidateTreeSHA256: payload.candidateTreeSHA256,
56
+ minimumOSVersion: payload.minimumOSVersion,
57
+ candidateApplicationMode: candidateMode,
58
+ candidateExecutableModes: candidateExecutableModes,
59
+ previousTreeSHA256: previousDigest,
60
+ previousApplicationMode: previousMode,
61
+ previousExecutableModes: previousExecutableModes,
62
+ previousApplicationPath: previousDigest == nil ? nil : previousPath,
63
+ candidateApplicationPath: candidatePath,
64
+ applicationPath: applicationPath
65
+ )
66
+ try DurableInstallJournal(transactionDirectory: stagedUpdate.directory).write(journal)
67
+ }
68
+
69
+ func activatePrepared(
70
+ stagedUpdate: StagedUpdate,
71
+ payload: ReleaseEnvelopePayload,
72
+ excludingAuthenticatedClientPID: pid_t
73
+ ) throws {
74
+ let durableJournal = DurableInstallJournal(transactionDirectory: stagedUpdate.directory)
75
+ guard var journal = try durableJournal.read(),
76
+ journal.phase == "prepared",
77
+ journal.transactionID == stagedUpdate.transactionID.uuidString.lowercased(),
78
+ journal.releaseID == payload.releaseID,
79
+ journal.releaseSequence == payload.releaseSequence,
80
+ journal.keyEpoch == payload.keyEpoch,
81
+ journal.artifactSHA256 == stagedUpdate.archive.sha256,
82
+ journal.manifestSHA256 == stagedUpdate.manifest.sha256,
83
+ journal.cohortPackageSHA256 == payload.packageSHA256,
84
+ journal.candidateTreeSHA256 == payload.candidateTreeSHA256,
85
+ journal.minimumOSVersion == payload.minimumOSVersion,
86
+ journal.envelopePayloadSHA256 == envelopeDigest(payload)
87
+ else {
88
+ throw AtomicActivationError.recoveryRequired
89
+ }
90
+
91
+ let namespace: ApplicationNamespace
92
+ do {
93
+ namespace = try ApplicationNamespace(journal: journal)
94
+ try ApplicationProcessQuiescence.requireQuiescence(
95
+ excludingAuthenticatedClientPID: excludingAuthenticatedClientPID
96
+ )
97
+ } catch is ApplicationProcessQuiescenceError {
98
+ throw AtomicActivationError.activationDeferred
99
+ } catch {
100
+ throw AtomicActivationError.recoveryRequired
101
+ }
102
+
103
+ do {
104
+ journal.phase = "launch-barrier-pending"
105
+ try durableJournal.write(journal)
106
+ try namespace.engageLaunchBarrier(journal: journal)
107
+ journal.phase = "launch-barrier-held"
108
+ try durableJournal.write(journal)
109
+ do {
110
+ try ApplicationProcessQuiescence.requireQuiescence(
111
+ excludingAuthenticatedClientPID: excludingAuthenticatedClientPID
112
+ )
113
+ } catch {
114
+ throw AtomicActivationError.activationDeferred
115
+ }
116
+
117
+ if let previousDigest = journal.previousTreeSHA256 {
118
+ guard ActivationRecoveryPolicy.commitAction(
119
+ snapshot: try Self.namespaceSnapshot(journal),
120
+ previousDigest: previousDigest,
121
+ candidateDigest: journal.candidateTreeSHA256
122
+ ) == .exchangeCandidateAndLive else {
123
+ throw AtomicActivationError.invalidPreparedState
124
+ }
125
+ journal.phase = "swap-pending"
126
+ try durableJournal.write(journal)
127
+ try namespace.exchangeCandidateAndLive()
128
+ guard ActivationRecoveryPolicy.commitAction(
129
+ snapshot: try Self.namespaceSnapshot(journal),
130
+ previousDigest: previousDigest,
131
+ candidateDigest: journal.candidateTreeSHA256
132
+ ) == .retainPrevious else {
133
+ throw AtomicActivationError.bindingChanged
134
+ }
135
+ journal.phase = "swapped"
136
+ try durableJournal.write(journal)
137
+
138
+ journal.phase = "previous-retaining"
139
+ try durableJournal.write(journal)
140
+ try namespace.retainSwappedPreviousExclusively()
141
+ guard ActivationRecoveryPolicy.commitAction(
142
+ snapshot: try Self.namespaceSnapshot(journal),
143
+ previousDigest: previousDigest,
144
+ candidateDigest: journal.candidateTreeSHA256
145
+ ) == .ready else {
146
+ throw AtomicActivationError.bindingChanged
147
+ }
148
+ journal.phase = "previous-retained"
149
+ try durableJournal.write(journal)
150
+ } else {
151
+ guard ActivationRecoveryPolicy.commitAction(
152
+ snapshot: try Self.namespaceSnapshot(journal),
153
+ previousDigest: nil,
154
+ candidateDigest: journal.candidateTreeSHA256
155
+ ) == .installCandidateExclusively else {
156
+ throw AtomicActivationError.invalidPreparedState
157
+ }
158
+ journal.phase = "first-install-pending"
159
+ try durableJournal.write(journal)
160
+ try namespace.installCandidateExclusively()
161
+ guard ActivationRecoveryPolicy.commitAction(
162
+ snapshot: try Self.namespaceSnapshot(journal),
163
+ previousDigest: nil,
164
+ candidateDigest: journal.candidateTreeSHA256
165
+ ) == .ready else {
166
+ throw AtomicActivationError.bindingChanged
167
+ }
168
+ journal.phase = "first-installed"
169
+ try durableJournal.write(journal)
170
+ }
171
+ journal.phase = "launch-barrier-releasing"
172
+ try durableJournal.write(journal)
173
+ try namespace.releaseCommittedLaunchBarrier(journal: journal)
174
+ try Self.requireCommittedNamespaceState(journal)
175
+ journal.phase = "launch-barrier-released"
176
+ try durableJournal.write(journal)
177
+ journal.phase = "activated"
178
+ try durableJournal.write(journal)
179
+ journal.phase = "committed"
180
+ try durableJournal.write(journal)
181
+ } catch {
182
+ if Self.requiresCommitRecovery(phase: journal.phase) {
183
+ // Once release of the launch barrier begins, the candidate may be
184
+ // executable. Never roll back underneath a process that could have
185
+ // started; startup recovery completes this exact bound candidate.
186
+ throw AtomicActivationError.recoveryRequired
187
+ }
188
+ do {
189
+ try rollback(
190
+ journal: &journal,
191
+ durableJournal: durableJournal,
192
+ namespace: namespace
193
+ )
194
+ } catch {
195
+ throw AtomicActivationError.recoveryRequired
196
+ }
197
+ if error is ApplicationProcessQuiescenceError {
198
+ throw AtomicActivationError.activationDeferred
199
+ }
200
+ if let activationError = error as? AtomicActivationError {
201
+ throw activationError
202
+ }
203
+ throw AtomicActivationError.namespaceMutationFailed
204
+ }
205
+ }
206
+
207
+ private func rollback(
208
+ journal: inout BrokerInstallJournal,
209
+ durableJournal: DurableInstallJournal,
210
+ namespace: ApplicationNamespace
211
+ ) throws {
212
+ journal.phase = "rollback-started"
213
+ try durableJournal.write(journal)
214
+ try Self.restoreRollbackTerminal(journal: journal, namespace: namespace)
215
+ try namespace.releaseRolledBackLaunchBarrier(journal: journal)
216
+ try Self.requireRollbackTerminalState(journal: journal)
217
+ journal.phase = "rolled-back"
218
+ try durableJournal.write(journal)
219
+ }
220
+
221
+ static func restoreRollbackTerminal(
222
+ journal: BrokerInstallJournal,
223
+ namespace: ApplicationNamespace
224
+ ) throws {
225
+ for _ in 0..<4 {
226
+ let snapshot = try namespaceSnapshot(journal)
227
+ switch ActivationRecoveryPolicy.rollbackAction(
228
+ snapshot: snapshot,
229
+ previousDigest: journal.previousTreeSHA256,
230
+ candidateDigest: journal.candidateTreeSHA256
231
+ ) {
232
+ case .exchangeCandidateAndLive:
233
+ try namespace.exchangeCandidateAndLive()
234
+ case .exchangePreviousAndLive:
235
+ try namespace.exchangeRetainedPreviousAndLive()
236
+ case .retainFailedFromPrevious:
237
+ try namespace.retainFailedCandidateFromPreviousSlotExclusively()
238
+ case .retainFailedFromLive:
239
+ try namespace.retainFailedCandidateFromLiveExclusively()
240
+ case .ready:
241
+ return
242
+ case .invalid:
243
+ throw AtomicActivationError.unprovenNamespaceDuringRollback
244
+ }
245
+ }
246
+ throw AtomicActivationError.unprovenNamespaceDuringRollback
247
+ }
248
+
249
+ static func requireRollbackTerminalState(journal: BrokerInstallJournal) throws {
250
+ let snapshot = try normalNamespaceSnapshot(journal)
251
+ guard ActivationRecoveryPolicy.rollbackAction(
252
+ snapshot: snapshot,
253
+ previousDigest: journal.previousTreeSHA256,
254
+ candidateDigest: journal.candidateTreeSHA256
255
+ ) == .ready else {
256
+ throw AtomicActivationError.unprovenNamespaceDuringRollback
257
+ }
258
+ }
259
+
260
+ static func normalNamespaceSnapshot(
261
+ _ journal: BrokerInstallJournal
262
+ ) throws -> ApplicationNamespaceSnapshot {
263
+ ApplicationNamespaceSnapshot(
264
+ live: try digestIfPresent(journal.applicationPath),
265
+ candidateSlot: try digestIfPresent(journal.candidateApplicationPath),
266
+ previousSlot: try digestIfPresent(journal.previousApplicationPath),
267
+ failedSlot: try digestIfPresent(failedCandidatePath(journal))
268
+ )
269
+ }
270
+
271
+ static func requireCommittedNamespaceState(journal: BrokerInstallJournal) throws {
272
+ guard try digestIfPresent(journal.applicationPath) == journal.candidateTreeSHA256,
273
+ try digestIfPresent(journal.candidateApplicationPath) == nil,
274
+ try digestIfPresent(journal.previousApplicationPath) == journal.previousTreeSHA256,
275
+ try digestIfPresent(failedCandidatePath(journal)) == nil
276
+ else {
277
+ throw AtomicActivationError.bindingChanged
278
+ }
279
+ }
280
+
281
+ static func namespaceSnapshot(
282
+ _ journal: BrokerInstallJournal
283
+ ) throws -> ApplicationNamespaceSnapshot {
284
+ ApplicationNamespaceSnapshot(
285
+ live: try barrierBoundDigestIfPresent(journal.applicationPath, journal: journal),
286
+ candidateSlot: try barrierBoundDigestIfPresent(
287
+ journal.candidateApplicationPath,
288
+ journal: journal
289
+ ),
290
+ previousSlot: try barrierBoundDigestIfPresent(
291
+ journal.previousApplicationPath,
292
+ journal: journal
293
+ ),
294
+ failedSlot: try barrierBoundDigestIfPresent(
295
+ failedCandidatePath(journal),
296
+ journal: journal
297
+ )
298
+ )
299
+ }
300
+
301
+ static func barrierBoundDigestIfPresent(
302
+ _ path: String?,
303
+ journal: BrokerInstallJournal
304
+ ) throws -> String? {
305
+ guard let path else { return nil }
306
+ var metadata = stat()
307
+ if lstat(path, &metadata) != 0 {
308
+ if errno == ENOENT { return nil }
309
+ throw AtomicActivationError.bindingChanged
310
+ }
311
+ let applicationMode = UInt16(metadata.st_mode & 0o777)
312
+ let allowedApplicationModes = Set(
313
+ [0, journal.candidateApplicationMode, journal.previousApplicationMode ?? 0]
314
+ )
315
+ guard allowedApplicationModes.contains(applicationMode) else {
316
+ throw AtomicActivationError.bindingChanged
317
+ }
318
+ var actualExecutableModes: [String: UInt16] = [:]
319
+ for relativePath in journal.candidateExecutableModes.keys {
320
+ var executableMetadata = stat()
321
+ guard lstat(path + "/" + relativePath, &executableMetadata) == 0,
322
+ (executableMetadata.st_mode & S_IFMT) == S_IFREG
323
+ else {
324
+ throw AtomicActivationError.bindingChanged
325
+ }
326
+ let executableMode = UInt16(executableMetadata.st_mode & 0o777)
327
+ let allowedExecutableModes = Set([
328
+ 0,
329
+ journal.candidateExecutableModes[relativePath] ?? 0,
330
+ journal.previousExecutableModes?[relativePath] ?? 0,
331
+ ])
332
+ guard allowedExecutableModes.contains(executableMode) else {
333
+ throw AtomicActivationError.bindingChanged
334
+ }
335
+ actualExecutableModes[relativePath] = executableMode
336
+ }
337
+ if applicationMode != 0,
338
+ actualExecutableModes.values.allSatisfy({ $0 != 0 }) {
339
+ return try CanonicalTree.digest(at: path)
340
+ }
341
+
342
+ var candidateOverrides = journal.candidateExecutableModes
343
+ candidateOverrides["."] = journal.candidateApplicationMode
344
+ let candidateDigest = try CanonicalTree.digest(
345
+ at: path,
346
+ modeOverrides: candidateOverrides
347
+ )
348
+ if candidateDigest == journal.candidateTreeSHA256 { return candidateDigest }
349
+ if let previousDigest = journal.previousTreeSHA256,
350
+ let previousMode = journal.previousApplicationMode {
351
+ guard var previousOverrides = journal.previousExecutableModes else {
352
+ throw AtomicActivationError.bindingChanged
353
+ }
354
+ previousOverrides["."] = previousMode
355
+ let restoredDigest = try CanonicalTree.digest(
356
+ at: path,
357
+ modeOverrides: previousOverrides
358
+ )
359
+ if restoredDigest == previousDigest { return restoredDigest }
360
+ }
361
+ throw AtomicActivationError.bindingChanged
362
+ }
363
+
364
+ static func digestIfPresent(_ path: String?) throws -> String? {
365
+ guard let path else { return nil }
366
+ var metadata = stat()
367
+ if lstat(path, &metadata) != 0 {
368
+ if errno == ENOENT { return nil }
369
+ throw AtomicActivationError.bindingChanged
370
+ }
371
+ return try CanonicalTree.digest(at: path)
372
+ }
373
+
374
+ static func applicationMode(_ path: String) throws -> UInt16 {
375
+ let descriptor = Darwin.open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
376
+ guard descriptor >= 0 else { throw AtomicActivationError.bindingChanged }
377
+ defer { Darwin.close(descriptor) }
378
+ var metadata = stat()
379
+ guard fstat(descriptor, &metadata) == 0,
380
+ (metadata.st_mode & S_IFMT) == S_IFDIR,
381
+ metadata.st_uid == 0,
382
+ (metadata.st_mode & 0o022) == 0,
383
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
384
+ else {
385
+ throw AtomicActivationError.bindingChanged
386
+ }
387
+ return UInt16(metadata.st_mode & 0o777)
388
+ }
389
+
390
+ static func applicationExecutableModes(
391
+ _ applicationPath: String
392
+ ) throws -> [String: UInt16] {
393
+ let paths = [
394
+ "Contents/MacOS/Recordings",
395
+ "Contents/Helpers/recordings",
396
+ "Contents/Helpers/recordings-update-client",
397
+ ]
398
+ var modes: [String: UInt16] = [:]
399
+ for relativePath in paths {
400
+ let descriptor = Darwin.open(
401
+ applicationPath + "/" + relativePath,
402
+ O_RDONLY | O_CLOEXEC | O_NOFOLLOW
403
+ )
404
+ guard descriptor >= 0 else { throw AtomicActivationError.bindingChanged }
405
+ var metadata = stat()
406
+ let valid = fstat(descriptor, &metadata) == 0 &&
407
+ (metadata.st_mode & S_IFMT) == S_IFREG &&
408
+ metadata.st_uid == 0 &&
409
+ (metadata.st_mode & 0o022) == 0 &&
410
+ metadata.st_nlink == 1 &&
411
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
412
+ Darwin.close(descriptor)
413
+ guard valid else { throw AtomicActivationError.bindingChanged }
414
+ modes[relativePath] = UInt16(metadata.st_mode & 0o777)
415
+ }
416
+ return modes
417
+ }
418
+
419
+ static func failedCandidatePath(_ journal: BrokerInstallJournal) -> String {
420
+ URL(fileURLWithPath: journal.candidateApplicationPath)
421
+ .deletingLastPathComponent()
422
+ .deletingLastPathComponent()
423
+ .appendingPathComponent("failed-candidate.app").path
424
+ }
425
+
426
+ private func validateApplicationsDirectory() throws {
427
+ guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
428
+ to: "/Applications"
429
+ ) else {
430
+ throw AtomicActivationError.unsafeApplicationsDirectory
431
+ }
432
+ let descriptor = Darwin.open("/Applications", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
433
+ guard descriptor >= 0 else { throw AtomicActivationError.unsafeApplicationsDirectory }
434
+ defer { Darwin.close(descriptor) }
435
+ guard DarwinACLValidator.descriptorIsSafeRootOwnedDirectory(
436
+ descriptor,
437
+ exactPath: "/Applications"
438
+ ) else {
439
+ throw AtomicActivationError.unsafeApplicationsDirectory
440
+ }
441
+ }
442
+
443
+ private func envelopeDigest(_ payload: ReleaseEnvelopePayload) throws -> String {
444
+ try SHA256.hash(data: payload.canonicalData())
445
+ .map { String(format: "%02x", $0) }
446
+ .joined()
447
+ }
448
+
449
+ private static func requiresCommitRecovery(phase: String) -> Bool {
450
+ phase == "launch-barrier-releasing" ||
451
+ phase == "launch-barrier-released" ||
452
+ phase == "activated" ||
453
+ phase == "committed"
454
+ }
455
+ }
456
+
457
+ enum AtomicActivationError: Error, CustomStringConvertible {
458
+ case unsafeApplicationsDirectory
459
+ case activationDeferred
460
+ case invalidPreparedState
461
+ case namespaceMutationFailed
462
+ case bindingChanged
463
+ case unprovenNamespaceDuringRollback
464
+ case recoveryRequired
465
+
466
+ var description: String {
467
+ switch self {
468
+ case .unsafeApplicationsDirectory: "The Applications directory is unsafe"
469
+ case .activationDeferred: "Activation is deferred until live-bundle processes exit"
470
+ case .invalidPreparedState: "The prepared activation namespace is invalid"
471
+ case .namespaceMutationFailed: "The atomic application namespace mutation failed"
472
+ case .bindingChanged: "An application tree changed across activation"
473
+ case .unprovenNamespaceDuringRollback: "Rollback found an unproven application namespace"
474
+ case .recoveryRequired: "Activation rollback requires broker startup recovery"
475
+ }
476
+ }
477
+ }