@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,662 @@
1
+ import Darwin
2
+ import Foundation
3
+ import RecordingsUpdateProtocol
4
+
5
+ enum BrokerStartupRecovery {
6
+ static func recoverInterruptedTransactions() throws {
7
+ guard geteuid() == 0 else { throw InstallRecoveryError.notRoot }
8
+ let rootState = try RootUpdateState()
9
+ try rootState.withExclusiveTransactionLock {
10
+ try recoverLocked(rootState: rootState)
11
+ }
12
+ }
13
+
14
+ private static func recoverLocked(rootState: RootUpdateState) throws {
15
+ let names = try FileManager.default.contentsOfDirectory(
16
+ atPath: RecordingsUpdateConstants.stateRoot
17
+ ).sorted()
18
+ let stateStore = try MonotonicReleaseStateStore()
19
+ for name in names where name.hasPrefix("transaction-") {
20
+ guard let identifier = UUID(uuidString: String(name.dropFirst("transaction-".count))),
21
+ "transaction-" + identifier.uuidString.lowercased() == name
22
+ else {
23
+ throw InstallRecoveryError.invalidTransactionDirectory
24
+ }
25
+ let directory = RecordingsUpdateConstants.stateRoot + "/" + name
26
+ try validateTransactionDirectory(directory)
27
+ let durableJournal = DurableInstallJournal(transactionDirectory: directory)
28
+ let current = try stateStore.currentState()
29
+ guard var journal = try durableJournal.read() else {
30
+ // An unjournaled transaction is inert only when there is no unresolved
31
+ // highest-seen barrier. Never silently prune the only possible evidence
32
+ // for a `seen` state created by an older or interrupted broker.
33
+ guard current?.phase != "seen" else {
34
+ throw InstallRecoveryError.missingJournalForSeenState
35
+ }
36
+ try rootState.removeTransactionDirectory(id: identifier)
37
+ continue
38
+ }
39
+ let terminalPhases: Set<String> = [
40
+ "rolled-back", "committed", "bootstrap-aborted", "bootstrap-committed",
41
+ ]
42
+ if !terminalPhases.contains(journal.phase) {
43
+ // A pre-hardening journal without signed minimum-OS evidence may
44
+ // be decoded for terminal cleanup, but it can never resume mutation.
45
+ guard let minimumOSVersion = journal.minimumOSVersion else {
46
+ throw InstallRecoveryError.invalidJournalPhase
47
+ }
48
+ try HostOSVersionPolicy.validate(
49
+ candidateMinimumOSVersion: minimumOSVersion,
50
+ hostProductVersion: try HostOSProductVersionReader.read()
51
+ )
52
+ }
53
+ if journal.resolvedOperation == .bootstrapCommit {
54
+ try recoverBootstrapCommit(
55
+ journal: &journal,
56
+ current: current,
57
+ durableJournal: durableJournal,
58
+ stateStore: stateStore
59
+ )
60
+ try rootState.removeTransactionDirectory(id: identifier)
61
+ continue
62
+ }
63
+ if journal.phase == "rolled-back" {
64
+ try requireRollbackTerminalState(journal)
65
+ switch try rolledBackBarrierAction(journal: journal, current: current) {
66
+ case .finalizeAbortThenPrune:
67
+ try stateStore.finalizeRecoveredAbort(journal: journal)
68
+ case .prune:
69
+ break
70
+ case .invalid:
71
+ throw InstallRecoveryError.monotonicStateMismatch
72
+ }
73
+ try rootState.removeTransactionDirectory(id: identifier)
74
+ continue
75
+ }
76
+ if journal.phase == "committed" {
77
+ try recoverCommitted(
78
+ journal: journal,
79
+ current: current,
80
+ stateStore: stateStore
81
+ )
82
+ try rootState.removeTransactionDirectory(id: identifier)
83
+ continue
84
+ }
85
+
86
+ if journal.phase == "prepared" {
87
+ switch try preparedBarrierAction(journal: journal, current: current) {
88
+ case .recover:
89
+ try recoverPreparedWithSeenBarrier(
90
+ journal: &journal,
91
+ durableJournal: durableJournal,
92
+ stateStore: stateStore
93
+ )
94
+ case .discard:
95
+ try discardPreparedBeforeSeen(
96
+ journal: &journal,
97
+ current: current,
98
+ durableJournal: durableJournal
99
+ )
100
+ case .invalid:
101
+ throw InstallRecoveryError.monotonicStateMismatch
102
+ }
103
+ } else {
104
+ try requireExactHighestSeen(journal: journal, current: current)
105
+ switch journal.phase {
106
+ case "launch-barrier-pending", "launch-barrier-held":
107
+ try recoverLaunchBarrierAndCommit(
108
+ journal: &journal,
109
+ durableJournal: durableJournal,
110
+ stateStore: stateStore
111
+ )
112
+ case "swap-pending":
113
+ try recoverSwapPending(journal: &journal, durableJournal: durableJournal)
114
+ try releaseCommittedLaunchBarrier(
115
+ journal: &journal,
116
+ durableJournal: durableJournal
117
+ )
118
+ try finishCommitted(
119
+ journal: &journal,
120
+ durableJournal: durableJournal,
121
+ stateStore: stateStore
122
+ )
123
+ case "swapped", "previous-retaining":
124
+ try recoverPreviousRetention(
125
+ journal: &journal,
126
+ durableJournal: durableJournal
127
+ )
128
+ try releaseCommittedLaunchBarrier(
129
+ journal: &journal,
130
+ durableJournal: durableJournal
131
+ )
132
+ try finishCommitted(
133
+ journal: &journal,
134
+ durableJournal: durableJournal,
135
+ stateStore: stateStore
136
+ )
137
+ case "previous-retained", "first-installed", "launch-barrier-releasing":
138
+ try releaseCommittedLaunchBarrier(
139
+ journal: &journal,
140
+ durableJournal: durableJournal
141
+ )
142
+ try finishCommitted(
143
+ journal: &journal,
144
+ durableJournal: durableJournal,
145
+ stateStore: stateStore
146
+ )
147
+ case "launch-barrier-released", "activated":
148
+ try finishCommitted(
149
+ journal: &journal,
150
+ durableJournal: durableJournal,
151
+ stateStore: stateStore
152
+ )
153
+ case "first-install-pending":
154
+ try recoverFirstInstallPending(
155
+ journal: &journal,
156
+ durableJournal: durableJournal
157
+ )
158
+ try releaseCommittedLaunchBarrier(
159
+ journal: &journal,
160
+ durableJournal: durableJournal
161
+ )
162
+ try finishCommitted(
163
+ journal: &journal,
164
+ durableJournal: durableJournal,
165
+ stateStore: stateStore
166
+ )
167
+ case "rollback-started":
168
+ try finishRollback(journal: journal)
169
+ journal.phase = "rolled-back"
170
+ try durableJournal.write(journal)
171
+ try stateStore.finalizeRecoveredAbort(journal: journal)
172
+ default:
173
+ throw InstallRecoveryError.invalidJournalPhase
174
+ }
175
+ }
176
+ guard journal.phase == "rolled-back" || journal.phase == "committed" else {
177
+ throw InstallRecoveryError.invalidJournalPhase
178
+ }
179
+ try rootState.removeTransactionDirectory(id: identifier)
180
+ }
181
+ }
182
+
183
+ private static func recoverBootstrapCommit(
184
+ journal: inout BrokerInstallJournal,
185
+ current: PersistedReleaseState?,
186
+ durableJournal: DurableInstallJournal,
187
+ stateStore: MonotonicReleaseStateStore
188
+ ) throws {
189
+ let journalPhase: BootstrapJournalPhase
190
+ switch journal.phase {
191
+ case "bootstrap-prepared": journalPhase = .prepared
192
+ case "bootstrap-commit-pending": journalPhase = .commitPending
193
+ case "bootstrap-aborted": journalPhase = .aborted
194
+ case "bootstrap-committed": journalPhase = .committed
195
+ default: throw InstallRecoveryError.invalidJournalPhase
196
+ }
197
+
198
+ let candidateMatches: Bool
199
+ if current == nil {
200
+ // Before `seen`, the broker has not performed a bootstrap state mutation.
201
+ // The exact root-owned journal may therefore be closed without trusting
202
+ // or re-reading a candidate that an administrator changed meanwhile.
203
+ candidateMatches = true
204
+ } else {
205
+ do {
206
+ candidateMatches = try CanonicalTree.digest(at: journal.applicationPath) ==
207
+ journal.candidateTreeSHA256
208
+ } catch {
209
+ candidateMatches = false
210
+ }
211
+ }
212
+
213
+ switch BootstrapRecoveryPolicy.action(
214
+ journalPhase: journalPhase,
215
+ currentPhase: try monotonicBarrierPhase(current),
216
+ exactBinding: exactReleaseBinding(journal: journal, current: current),
217
+ priorMonotonicStateWasAbsent: journal.bootstrapPriorMonotonicState == "absent",
218
+ candidateMatches: candidateMatches
219
+ ) {
220
+ case .discardBeforeSeen:
221
+ journal.phase = "bootstrap-aborted"
222
+ try durableJournal.write(journal)
223
+ case .finalizeCommit:
224
+ if journal.phase == "bootstrap-prepared" {
225
+ journal.phase = "bootstrap-commit-pending"
226
+ try durableJournal.write(journal)
227
+ }
228
+ try stateStore.finalizeRecoveredCommit(journal: journal)
229
+ journal.phase = "bootstrap-committed"
230
+ try durableJournal.write(journal)
231
+ case .validateCommitted:
232
+ guard current?.phase == "committed" else {
233
+ throw InstallRecoveryError.monotonicStateMismatch
234
+ }
235
+ journal.phase = "bootstrap-committed"
236
+ try durableJournal.write(journal)
237
+ case .prune:
238
+ break
239
+ case .invalid:
240
+ throw InstallRecoveryError.monotonicStateMismatch
241
+ }
242
+ }
243
+
244
+ private static func recoverPreparedWithSeenBarrier(
245
+ journal: inout BrokerInstallJournal,
246
+ durableJournal: DurableInstallJournal,
247
+ stateStore: MonotonicReleaseStateStore
248
+ ) throws {
249
+ journal.phase = "launch-barrier-pending"
250
+ try durableJournal.write(journal)
251
+ try recoverLaunchBarrierAndCommit(
252
+ journal: &journal,
253
+ durableJournal: durableJournal,
254
+ stateStore: stateStore
255
+ )
256
+ }
257
+
258
+ private static func recoverLaunchBarrierAndCommit(
259
+ journal: inout BrokerInstallJournal,
260
+ durableJournal: DurableInstallJournal,
261
+ stateStore: MonotonicReleaseStateStore
262
+ ) throws {
263
+ let namespace = try applicationNamespace(journal)
264
+ try mapNamespaceMutation { try namespace.engageLaunchBarrier(journal: journal) }
265
+ journal.phase = "launch-barrier-held"
266
+ try durableJournal.write(journal)
267
+ try requireQuiescence()
268
+ if journal.previousTreeSHA256 == nil {
269
+ journal.phase = "first-install-pending"
270
+ try durableJournal.write(journal)
271
+ try recoverFirstInstallPending(journal: &journal, durableJournal: durableJournal)
272
+ } else {
273
+ journal.phase = "swap-pending"
274
+ try durableJournal.write(journal)
275
+ try recoverSwapPending(journal: &journal, durableJournal: durableJournal)
276
+ }
277
+ try releaseCommittedLaunchBarrier(
278
+ journal: &journal,
279
+ durableJournal: durableJournal
280
+ )
281
+ try finishCommitted(
282
+ journal: &journal,
283
+ durableJournal: durableJournal,
284
+ stateStore: stateStore
285
+ )
286
+ }
287
+
288
+ private static func recoverSwapPending(
289
+ journal: inout BrokerInstallJournal,
290
+ durableJournal: DurableInstallJournal
291
+ ) throws {
292
+ guard let previousDigest = journal.previousTreeSHA256 else {
293
+ throw InstallRecoveryError.invalidJournalPhase
294
+ }
295
+ let namespace = try applicationNamespace(journal)
296
+ switch ActivationRecoveryPolicy.commitAction(
297
+ snapshot: try namespaceSnapshot(journal),
298
+ previousDigest: previousDigest,
299
+ candidateDigest: journal.candidateTreeSHA256
300
+ ) {
301
+ case .exchangeCandidateAndLive:
302
+ try requireQuiescence()
303
+ try mapNamespaceMutation { try namespace.exchangeCandidateAndLive() }
304
+ case .retainPrevious, .ready:
305
+ // The exchange or subsequent retain completed before its phase write.
306
+ break
307
+ case .installCandidateExclusively, .invalid:
308
+ throw InstallRecoveryError.ambiguousApplicationState
309
+ }
310
+ journal.phase = "swapped"
311
+ try durableJournal.write(journal)
312
+ try recoverPreviousRetention(journal: &journal, durableJournal: durableJournal)
313
+ }
314
+
315
+ private static func recoverPreviousRetention(
316
+ journal: inout BrokerInstallJournal,
317
+ durableJournal: DurableInstallJournal
318
+ ) throws {
319
+ guard let previousDigest = journal.previousTreeSHA256 else {
320
+ throw InstallRecoveryError.ambiguousApplicationState
321
+ }
322
+ let namespace = try applicationNamespace(journal)
323
+ switch ActivationRecoveryPolicy.commitAction(
324
+ snapshot: try namespaceSnapshot(journal),
325
+ previousDigest: previousDigest,
326
+ candidateDigest: journal.candidateTreeSHA256
327
+ ) {
328
+ case .retainPrevious:
329
+ journal.phase = "previous-retaining"
330
+ try durableJournal.write(journal)
331
+ try mapNamespaceMutation { try namespace.retainSwappedPreviousExclusively() }
332
+ case .ready:
333
+ // Retain completed before the durable phase update.
334
+ break
335
+ case .exchangeCandidateAndLive, .installCandidateExclusively, .invalid:
336
+ throw InstallRecoveryError.ambiguousApplicationState
337
+ }
338
+ try requireBarrierHeldCommittedNamespaceState(journal)
339
+ journal.phase = "previous-retained"
340
+ try durableJournal.write(journal)
341
+ }
342
+
343
+ private static func recoverFirstInstallPending(
344
+ journal: inout BrokerInstallJournal,
345
+ durableJournal: DurableInstallJournal
346
+ ) throws {
347
+ guard journal.previousTreeSHA256 == nil else {
348
+ throw InstallRecoveryError.invalidJournalPhase
349
+ }
350
+ let namespace = try applicationNamespace(journal)
351
+ switch ActivationRecoveryPolicy.commitAction(
352
+ snapshot: try namespaceSnapshot(journal),
353
+ previousDigest: nil,
354
+ candidateDigest: journal.candidateTreeSHA256
355
+ ) {
356
+ case .installCandidateExclusively:
357
+ try requireQuiescence()
358
+ try mapNamespaceMutation { try namespace.installCandidateExclusively() }
359
+ case .ready:
360
+ // Exclusive install completed before its phase write.
361
+ break
362
+ case .exchangeCandidateAndLive, .retainPrevious, .invalid:
363
+ throw InstallRecoveryError.ambiguousApplicationState
364
+ }
365
+ try requireBarrierHeldCommittedNamespaceState(journal)
366
+ journal.phase = "first-installed"
367
+ try durableJournal.write(journal)
368
+ }
369
+
370
+ private static func discardPreparedBeforeSeen(
371
+ journal: inout BrokerInstallJournal,
372
+ current: PersistedReleaseState?,
373
+ durableJournal: DurableInstallJournal
374
+ ) throws {
375
+ if let current {
376
+ let exactAborted = current.phase == "aborted" &&
377
+ current.keyEpoch == journal.keyEpoch &&
378
+ current.releaseSequence == journal.releaseSequence &&
379
+ current.releaseID == journal.releaseID &&
380
+ current.envelopePayloadSHA256 == journal.envelopePayloadSHA256
381
+ let olderTerminal = (current.phase == "committed" || current.phase == "aborted") &&
382
+ current.keyEpoch == journal.keyEpoch &&
383
+ current.cohortPackageSHA256 == journal.cohortPackageSHA256 &&
384
+ current.releaseSequence < journal.releaseSequence
385
+ guard exactAborted || olderTerminal else {
386
+ throw InstallRecoveryError.monotonicStateMismatch
387
+ }
388
+ }
389
+ guard try digestIfPresent(journal.applicationPath) == journal.previousTreeSHA256,
390
+ try digestIfPresent(journal.candidateApplicationPath) == journal.candidateTreeSHA256,
391
+ try digestIfPresent(journal.previousApplicationPath) == nil,
392
+ try digestIfPresent(failedCandidatePath(journal)) == nil
393
+ else {
394
+ throw InstallRecoveryError.ambiguousApplicationState
395
+ }
396
+ journal.phase = "rolled-back"
397
+ try durableJournal.write(journal)
398
+ }
399
+
400
+ private static func finishRollback(journal: BrokerInstallJournal) throws {
401
+ let namespace = try applicationNamespace(journal)
402
+ do {
403
+ try AtomicActivator.restoreRollbackTerminal(journal: journal, namespace: namespace)
404
+ try namespace.releaseRolledBackLaunchBarrier(journal: journal)
405
+ try AtomicActivator.requireRollbackTerminalState(journal: journal)
406
+ } catch {
407
+ throw InstallRecoveryError.rollbackFailed
408
+ }
409
+ }
410
+
411
+ private static func requireRollbackTerminalState(_ journal: BrokerInstallJournal) throws {
412
+ do {
413
+ try AtomicActivator.requireRollbackTerminalState(journal: journal)
414
+ } catch {
415
+ throw InstallRecoveryError.rollbackFailed
416
+ }
417
+ }
418
+
419
+ private static func requireCommittedNamespaceState(_ journal: BrokerInstallJournal) throws {
420
+ do { try AtomicActivator.requireCommittedNamespaceState(journal: journal) }
421
+ catch { throw InstallRecoveryError.ambiguousApplicationState }
422
+ }
423
+
424
+ private static func requireBarrierHeldCommittedNamespaceState(
425
+ _ journal: BrokerInstallJournal
426
+ ) throws {
427
+ let action = ActivationRecoveryPolicy.commitAction(
428
+ snapshot: try namespaceSnapshot(journal),
429
+ previousDigest: journal.previousTreeSHA256,
430
+ candidateDigest: journal.candidateTreeSHA256
431
+ )
432
+ guard action == .ready else {
433
+ throw InstallRecoveryError.ambiguousApplicationState
434
+ }
435
+ }
436
+
437
+ private static func releaseCommittedLaunchBarrier(
438
+ journal: inout BrokerInstallJournal,
439
+ durableJournal: DurableInstallJournal
440
+ ) throws {
441
+ journal.phase = "launch-barrier-releasing"
442
+ try durableJournal.write(journal)
443
+ let namespace = try applicationNamespace(journal)
444
+ try mapNamespaceMutation {
445
+ try namespace.releaseCommittedLaunchBarrier(journal: journal)
446
+ }
447
+ try requireCommittedNamespaceState(journal)
448
+ journal.phase = "launch-barrier-released"
449
+ try durableJournal.write(journal)
450
+ }
451
+
452
+ private static func validateTransactionDirectory(_ path: String) throws {
453
+ guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(to: path) else {
454
+ throw InstallRecoveryError.invalidTransactionDirectory
455
+ }
456
+ var metadata = stat()
457
+ guard lstat(path, &metadata) == 0,
458
+ (metadata.st_mode & S_IFMT) == S_IFDIR,
459
+ metadata.st_uid == 0,
460
+ (metadata.st_mode & 0o077) == 0
461
+ else {
462
+ throw InstallRecoveryError.invalidTransactionDirectory
463
+ }
464
+ let descriptor = Darwin.open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
465
+ guard descriptor >= 0 else { throw InstallRecoveryError.invalidTransactionDirectory }
466
+ defer { Darwin.close(descriptor) }
467
+ var opened = stat()
468
+ guard fstat(descriptor, &opened) == 0,
469
+ opened.st_dev == metadata.st_dev,
470
+ opened.st_ino == metadata.st_ino,
471
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
472
+ else {
473
+ throw InstallRecoveryError.invalidTransactionDirectory
474
+ }
475
+ }
476
+
477
+ private static func preparedBarrierAction(
478
+ journal: BrokerInstallJournal,
479
+ current: PersistedReleaseState?
480
+ ) throws -> PreparedBarrierRecoveryAction {
481
+ ActivationRecoveryPolicy.preparedBarrierAction(
482
+ currentPhase: try monotonicBarrierPhase(current),
483
+ currentSequence: current?.releaseSequence,
484
+ journalSequence: journal.releaseSequence,
485
+ exactBinding: exactReleaseBinding(journal: journal, current: current),
486
+ sameCohort: sameCohort(journal: journal, current: current)
487
+ )
488
+ }
489
+
490
+ private static func rolledBackBarrierAction(
491
+ journal: BrokerInstallJournal,
492
+ current: PersistedReleaseState?
493
+ ) throws -> RolledBackBarrierRecoveryAction {
494
+ ActivationRecoveryPolicy.rolledBackBarrierAction(
495
+ currentPhase: try monotonicBarrierPhase(current),
496
+ currentSequence: current?.releaseSequence,
497
+ journalSequence: journal.releaseSequence,
498
+ exactBinding: exactReleaseBinding(journal: journal, current: current),
499
+ sameCohort: sameCohort(journal: journal, current: current)
500
+ )
501
+ }
502
+
503
+ private static func monotonicBarrierPhase(
504
+ _ current: PersistedReleaseState?
505
+ ) throws -> MonotonicBarrierPhase? {
506
+ guard let current else { return nil }
507
+ switch current.phase {
508
+ case "seen": return .seen
509
+ case "aborted": return .aborted
510
+ case "committed": return .committed
511
+ default: throw InstallRecoveryError.monotonicStateMismatch
512
+ }
513
+ }
514
+
515
+ private static func exactReleaseBinding(
516
+ journal: BrokerInstallJournal,
517
+ current: PersistedReleaseState?
518
+ ) -> Bool {
519
+ guard let current else { return false }
520
+ return current.purpose == journal.expectedPurpose &&
521
+ current.keyEpoch == journal.keyEpoch &&
522
+ current.releaseSequence == journal.releaseSequence &&
523
+ current.releaseID == journal.releaseID &&
524
+ current.cohortPackageSHA256 == journal.cohortPackageSHA256 &&
525
+ current.envelopePayloadSHA256 == journal.envelopePayloadSHA256
526
+ }
527
+
528
+ private static func sameCohort(
529
+ journal: BrokerInstallJournal,
530
+ current: PersistedReleaseState?
531
+ ) -> Bool {
532
+ guard let current else { return false }
533
+ return current.keyEpoch == journal.keyEpoch &&
534
+ current.cohortPackageSHA256 == journal.cohortPackageSHA256
535
+ }
536
+
537
+ private static func recoverCommitted(
538
+ journal: BrokerInstallJournal,
539
+ current: PersistedReleaseState?,
540
+ stateStore: MonotonicReleaseStateStore
541
+ ) throws {
542
+ guard let current,
543
+ current.releaseSequence >= journal.releaseSequence
544
+ else {
545
+ throw InstallRecoveryError.monotonicStateMismatch
546
+ }
547
+ if current.releaseSequence > journal.releaseSequence { return }
548
+ try requireExactReleaseBinding(journal: journal, current: current)
549
+ try requireCommittedNamespaceState(journal)
550
+ try stateStore.finalizeRecoveredCommit(journal: journal)
551
+ }
552
+
553
+ private static func exactHighestSeen(
554
+ journal: BrokerInstallJournal,
555
+ current: PersistedReleaseState?
556
+ ) -> Bool {
557
+ guard let current, current.phase == "seen" else { return false }
558
+ return current.purpose == journal.expectedPurpose &&
559
+ current.keyEpoch == journal.keyEpoch &&
560
+ current.releaseSequence == journal.releaseSequence &&
561
+ current.releaseID == journal.releaseID &&
562
+ current.cohortPackageSHA256 == journal.cohortPackageSHA256 &&
563
+ current.envelopePayloadSHA256 == journal.envelopePayloadSHA256
564
+ }
565
+
566
+ private static func requireExactHighestSeen(
567
+ journal: BrokerInstallJournal,
568
+ current: PersistedReleaseState?
569
+ ) throws {
570
+ guard exactHighestSeen(journal: journal, current: current) else {
571
+ throw InstallRecoveryError.monotonicStateMismatch
572
+ }
573
+ }
574
+
575
+ private static func requireExactReleaseBinding(
576
+ journal: BrokerInstallJournal,
577
+ current: PersistedReleaseState
578
+ ) throws {
579
+ guard current.purpose == journal.expectedPurpose,
580
+ current.keyEpoch == journal.keyEpoch,
581
+ current.releaseSequence == journal.releaseSequence,
582
+ current.releaseID == journal.releaseID,
583
+ current.cohortPackageSHA256 == journal.cohortPackageSHA256,
584
+ current.envelopePayloadSHA256 == journal.envelopePayloadSHA256
585
+ else {
586
+ throw InstallRecoveryError.monotonicStateMismatch
587
+ }
588
+ }
589
+
590
+ private static func finishCommitted(
591
+ journal: inout BrokerInstallJournal,
592
+ durableJournal: DurableInstallJournal,
593
+ stateStore: MonotonicReleaseStateStore
594
+ ) throws {
595
+ try requireCommittedNamespaceState(journal)
596
+ journal.phase = "committed"
597
+ try durableJournal.write(journal)
598
+ try stateStore.finalizeRecoveredCommit(journal: journal)
599
+ }
600
+
601
+ private static func applicationNamespace(_ journal: BrokerInstallJournal) throws -> ApplicationNamespace {
602
+ do { return try ApplicationNamespace(journal: journal) }
603
+ catch { throw InstallRecoveryError.invalidTransactionDirectory }
604
+ }
605
+
606
+ private static func mapNamespaceMutation(_ operation: () throws -> Void) throws {
607
+ do { try operation() }
608
+ catch { throw InstallRecoveryError.namespaceMutationFailed }
609
+ }
610
+
611
+ private static func requireQuiescence() throws {
612
+ do {
613
+ try ApplicationProcessQuiescence.requireQuiescence(
614
+ excludingAuthenticatedClientPID: nil
615
+ )
616
+ } catch {
617
+ throw InstallRecoveryError.activationDeferred
618
+ }
619
+ }
620
+
621
+ private static func digestIfPresent(_ path: String?) throws -> String? {
622
+ do { return try AtomicActivator.digestIfPresent(path) }
623
+ catch { throw InstallRecoveryError.ambiguousApplicationState }
624
+ }
625
+
626
+ private static func namespaceSnapshot(
627
+ _ journal: BrokerInstallJournal
628
+ ) throws -> ApplicationNamespaceSnapshot {
629
+ do { return try AtomicActivator.namespaceSnapshot(journal) }
630
+ catch { throw InstallRecoveryError.ambiguousApplicationState }
631
+ }
632
+
633
+ private static func failedCandidatePath(_ journal: BrokerInstallJournal) -> String {
634
+ AtomicActivator.failedCandidatePath(journal)
635
+ }
636
+ }
637
+
638
+ enum InstallRecoveryError: Error, CustomStringConvertible {
639
+ case notRoot
640
+ case invalidTransactionDirectory
641
+ case invalidJournalPhase
642
+ case missingJournalForSeenState
643
+ case monotonicStateMismatch
644
+ case ambiguousApplicationState
645
+ case activationDeferred
646
+ case namespaceMutationFailed
647
+ case rollbackFailed
648
+
649
+ var description: String {
650
+ switch self {
651
+ case .notRoot: "The update broker is not running as root during recovery"
652
+ case .invalidTransactionDirectory: "A protected update transaction directory is unsafe"
653
+ case .invalidJournalPhase: "An install journal has an unsupported recovery phase"
654
+ case .missingJournalForSeenState: "Highest-seen state has no recoverable transaction journal"
655
+ case .monotonicStateMismatch: "An interrupted transaction does not match highest-seen state"
656
+ case .ambiguousApplicationState: "Interrupted recovery found an ambiguous application namespace"
657
+ case .activationDeferred: "Recovery is deferred until live-bundle processes exit"
658
+ case .namespaceMutationFailed: "Recovery could not mutate the application namespace atomically"
659
+ case .rollbackFailed: "Interrupted recovery could not restore the exact previous tree"
660
+ }
661
+ }
662
+ }