@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,463 @@
1
+ import Darwin
2
+ import Foundation
3
+ import RecordingsUpdateProtocol
4
+
5
+ /// Descriptor-bound namespace mutations for the live application and one protected
6
+ /// transaction. Existing installs are exchanged with the candidate in one kernel
7
+ /// operation; first installs use exclusive rename semantics and can never replace a
8
+ /// leaf created concurrently in `/Applications`.
9
+ final class ApplicationNamespace {
10
+ private static let liveLeaf = "Recordings.app"
11
+ private static let previousLeaf = "previous.app"
12
+ private static let failedCandidateLeaf = "failed-candidate.app"
13
+
14
+ private let applicationsDescriptor: Int32
15
+ private let candidateDescriptor: Int32
16
+ private let transactionDescriptor: Int32
17
+
18
+ init(journal: BrokerInstallJournal) throws {
19
+ let expectedCandidateDirectory = journal.candidateApplicationPath
20
+ .deletingLastPathComponent
21
+ let expectedTransactionDirectory = expectedCandidateDirectory
22
+ .deletingLastPathComponent
23
+ guard journal.applicationPath == RecordingsUpdateConstants.applicationPath,
24
+ journal.candidateApplicationPath == expectedTransactionDirectory
25
+ + "/candidate/Recordings.app",
26
+ journal.previousApplicationPath == nil ||
27
+ journal.previousApplicationPath == expectedTransactionDirectory + "/previous.app"
28
+ else {
29
+ throw ApplicationNamespaceError.invalidBinding
30
+ }
31
+
32
+ applicationsDescriptor = try Self.openValidatedDirectory(
33
+ "/Applications",
34
+ exactApplicationsDirectory: true
35
+ )
36
+ do {
37
+ candidateDescriptor = try Self.openValidatedDirectory(
38
+ expectedCandidateDirectory,
39
+ exactApplicationsDirectory: false
40
+ )
41
+ } catch {
42
+ Darwin.close(applicationsDescriptor)
43
+ throw error
44
+ }
45
+ do {
46
+ transactionDescriptor = try Self.openValidatedDirectory(
47
+ expectedTransactionDirectory,
48
+ exactApplicationsDirectory: false
49
+ )
50
+ } catch {
51
+ Darwin.close(candidateDescriptor)
52
+ Darwin.close(applicationsDescriptor)
53
+ throw error
54
+ }
55
+ }
56
+
57
+ deinit {
58
+ Darwin.close(transactionDescriptor)
59
+ Darwin.close(candidateDescriptor)
60
+ Darwin.close(applicationsDescriptor)
61
+ }
62
+
63
+ func exchangeCandidateAndLive() throws {
64
+ try renameAtX(
65
+ from: candidateDescriptor,
66
+ Self.liveLeaf,
67
+ to: applicationsDescriptor,
68
+ Self.liveLeaf,
69
+ flags: UInt32(RENAME_SWAP)
70
+ )
71
+ try synchronize([candidateDescriptor, applicationsDescriptor])
72
+ }
73
+
74
+ func installCandidateExclusively() throws {
75
+ try renameAtX(
76
+ from: candidateDescriptor,
77
+ Self.liveLeaf,
78
+ to: applicationsDescriptor,
79
+ Self.liveLeaf,
80
+ flags: UInt32(RENAME_EXCL)
81
+ )
82
+ try synchronize([candidateDescriptor, applicationsDescriptor])
83
+ }
84
+
85
+ func retainSwappedPreviousExclusively() throws {
86
+ try renameAtX(
87
+ from: candidateDescriptor,
88
+ Self.liveLeaf,
89
+ to: transactionDescriptor,
90
+ Self.previousLeaf,
91
+ flags: UInt32(RENAME_EXCL)
92
+ )
93
+ try synchronize([candidateDescriptor, transactionDescriptor])
94
+ }
95
+
96
+ func exchangeRetainedPreviousAndLive() throws {
97
+ try renameAtX(
98
+ from: transactionDescriptor,
99
+ Self.previousLeaf,
100
+ to: applicationsDescriptor,
101
+ Self.liveLeaf,
102
+ flags: UInt32(RENAME_SWAP)
103
+ )
104
+ try synchronize([transactionDescriptor, applicationsDescriptor])
105
+ }
106
+
107
+ func retainFailedCandidateFromPreviousSlotExclusively() throws {
108
+ try renameAtX(
109
+ from: transactionDescriptor,
110
+ Self.previousLeaf,
111
+ to: transactionDescriptor,
112
+ Self.failedCandidateLeaf,
113
+ flags: UInt32(RENAME_EXCL)
114
+ )
115
+ try synchronize([transactionDescriptor])
116
+ }
117
+
118
+ func retainFailedCandidateFromLiveExclusively() throws {
119
+ try renameAtX(
120
+ from: applicationsDescriptor,
121
+ Self.liveLeaf,
122
+ to: transactionDescriptor,
123
+ Self.failedCandidateLeaf,
124
+ flags: UInt32(RENAME_EXCL)
125
+ )
126
+ try synchronize([applicationsDescriptor, transactionDescriptor])
127
+ }
128
+
129
+ func engageLaunchBarrier(journal: BrokerInstallJournal) throws {
130
+ try setApplicationExecutableModes(
131
+ parent: candidateDescriptor,
132
+ applicationLeaf: Self.liveLeaf,
133
+ originalModes: journal.candidateExecutableModes,
134
+ engageBarrier: true
135
+ )
136
+ try setDirectoryMode(
137
+ parent: candidateDescriptor,
138
+ leaf: Self.liveLeaf,
139
+ allowedCurrentModes: [mode_t(journal.candidateApplicationMode), 0],
140
+ newMode: 0
141
+ )
142
+ if let previousMode = journal.previousApplicationMode {
143
+ guard let previousExecutableModes = journal.previousExecutableModes else {
144
+ throw ApplicationNamespaceError.invalidBinding
145
+ }
146
+ try setApplicationExecutableModes(
147
+ parent: applicationsDescriptor,
148
+ applicationLeaf: Self.liveLeaf,
149
+ originalModes: previousExecutableModes,
150
+ engageBarrier: true
151
+ )
152
+ try setDirectoryMode(
153
+ parent: applicationsDescriptor,
154
+ leaf: Self.liveLeaf,
155
+ allowedCurrentModes: [mode_t(previousMode), 0],
156
+ newMode: 0
157
+ )
158
+ }
159
+ }
160
+
161
+ func releaseCommittedLaunchBarrier(journal: BrokerInstallJournal) throws {
162
+ try setApplicationExecutableModes(
163
+ parent: applicationsDescriptor,
164
+ applicationLeaf: Self.liveLeaf,
165
+ originalModes: journal.candidateExecutableModes,
166
+ engageBarrier: false
167
+ )
168
+ try setDirectoryMode(
169
+ parent: applicationsDescriptor,
170
+ leaf: Self.liveLeaf,
171
+ allowedCurrentModes: [0, mode_t(journal.candidateApplicationMode)],
172
+ newMode: mode_t(journal.candidateApplicationMode)
173
+ )
174
+ if let previousMode = journal.previousApplicationMode {
175
+ guard let previousExecutableModes = journal.previousExecutableModes else {
176
+ throw ApplicationNamespaceError.invalidBinding
177
+ }
178
+ try setApplicationExecutableModes(
179
+ parent: transactionDescriptor,
180
+ applicationLeaf: Self.previousLeaf,
181
+ originalModes: previousExecutableModes,
182
+ engageBarrier: false
183
+ )
184
+ try setDirectoryMode(
185
+ parent: transactionDescriptor,
186
+ leaf: Self.previousLeaf,
187
+ allowedCurrentModes: [0, mode_t(previousMode)],
188
+ newMode: mode_t(previousMode)
189
+ )
190
+ }
191
+ }
192
+
193
+ func releaseRolledBackLaunchBarrier(journal: BrokerInstallJournal) throws {
194
+ if let previousMode = journal.previousApplicationMode {
195
+ guard let previousExecutableModes = journal.previousExecutableModes else {
196
+ throw ApplicationNamespaceError.invalidBinding
197
+ }
198
+ try setApplicationExecutableModes(
199
+ parent: applicationsDescriptor,
200
+ applicationLeaf: Self.liveLeaf,
201
+ originalModes: previousExecutableModes,
202
+ engageBarrier: false
203
+ )
204
+ try setDirectoryMode(
205
+ parent: applicationsDescriptor,
206
+ leaf: Self.liveLeaf,
207
+ allowedCurrentModes: [0, mode_t(previousMode)],
208
+ newMode: mode_t(previousMode)
209
+ )
210
+ }
211
+ let candidateAtOriginalSlot = try directoryExists(
212
+ parent: candidateDescriptor,
213
+ leaf: Self.liveLeaf
214
+ )
215
+ let candidateAtFailedSlot = try directoryExists(
216
+ parent: transactionDescriptor,
217
+ leaf: Self.failedCandidateLeaf
218
+ )
219
+ guard candidateAtOriginalSlot != candidateAtFailedSlot else {
220
+ throw ApplicationNamespaceError.invalidBinding
221
+ }
222
+ try setApplicationExecutableModes(
223
+ parent: candidateAtOriginalSlot ? candidateDescriptor : transactionDescriptor,
224
+ applicationLeaf: candidateAtOriginalSlot ? Self.liveLeaf : Self.failedCandidateLeaf,
225
+ originalModes: journal.candidateExecutableModes,
226
+ engageBarrier: false
227
+ )
228
+ try setDirectoryMode(
229
+ parent: candidateAtOriginalSlot ? candidateDescriptor : transactionDescriptor,
230
+ leaf: candidateAtOriginalSlot ? Self.liveLeaf : Self.failedCandidateLeaf,
231
+ allowedCurrentModes: [0, mode_t(journal.candidateApplicationMode)],
232
+ newMode: mode_t(journal.candidateApplicationMode)
233
+ )
234
+ }
235
+
236
+ private func renameAtX(
237
+ from sourceDescriptor: Int32,
238
+ _ sourceLeaf: String,
239
+ to destinationDescriptor: Int32,
240
+ _ destinationLeaf: String,
241
+ flags: UInt32
242
+ ) throws {
243
+ let status = sourceLeaf.withCString { source in
244
+ destinationLeaf.withCString { destination in
245
+ renameatx_np(
246
+ sourceDescriptor,
247
+ source,
248
+ destinationDescriptor,
249
+ destination,
250
+ flags
251
+ )
252
+ }
253
+ }
254
+ guard status == 0 else { throw ApplicationNamespaceError.mutationFailed }
255
+ }
256
+
257
+ private func synchronize(_ descriptors: [Int32]) throws {
258
+ for descriptor in descriptors {
259
+ guard DarwinACLValidator.descriptorHasNoExtendedACL(descriptor),
260
+ fsync(descriptor) == 0
261
+ else {
262
+ throw ApplicationNamespaceError.couldNotSync
263
+ }
264
+ }
265
+ }
266
+
267
+ private func setDirectoryMode(
268
+ parent: Int32,
269
+ leaf: String,
270
+ allowedCurrentModes: Set<mode_t>,
271
+ newMode: mode_t
272
+ ) throws {
273
+ var namedBefore = stat()
274
+ let inspectStatus = leaf.withCString {
275
+ fstatat(parent, $0, &namedBefore, AT_SYMLINK_NOFOLLOW)
276
+ }
277
+ guard inspectStatus == 0,
278
+ (namedBefore.st_mode & S_IFMT) == S_IFDIR,
279
+ namedBefore.st_uid == 0,
280
+ allowedCurrentModes.contains(namedBefore.st_mode & 0o777)
281
+ else {
282
+ throw ApplicationNamespaceError.invalidBinding
283
+ }
284
+ let descriptor = leaf.withCString {
285
+ openat(parent, $0, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
286
+ }
287
+ guard descriptor >= 0 else { throw ApplicationNamespaceError.invalidBinding }
288
+ defer { Darwin.close(descriptor) }
289
+ var opened = stat()
290
+ guard fstat(descriptor, &opened) == 0,
291
+ opened.st_dev == namedBefore.st_dev,
292
+ opened.st_ino == namedBefore.st_ino,
293
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor),
294
+ fchmod(descriptor, newMode) == 0,
295
+ fsync(descriptor) == 0
296
+ else {
297
+ throw ApplicationNamespaceError.mutationFailed
298
+ }
299
+ var namedAfter = stat()
300
+ let recheckStatus = leaf.withCString {
301
+ fstatat(parent, $0, &namedAfter, AT_SYMLINK_NOFOLLOW)
302
+ }
303
+ guard recheckStatus == 0,
304
+ namedAfter.st_dev == opened.st_dev,
305
+ namedAfter.st_ino == opened.st_ino,
306
+ (namedAfter.st_mode & 0o777) == newMode,
307
+ fsync(parent) == 0
308
+ else {
309
+ throw ApplicationNamespaceError.mutationFailed
310
+ }
311
+ }
312
+
313
+ private func setApplicationExecutableModes(
314
+ parent: Int32,
315
+ applicationLeaf: String,
316
+ originalModes: [String: UInt16],
317
+ engageBarrier: Bool
318
+ ) throws {
319
+ let application = try openSafeDirectoryAt(parent: parent, leaf: applicationLeaf)
320
+ defer { Darwin.close(application) }
321
+ for relativePath in originalModes.keys.sorted() {
322
+ let components = relativePath.split(separator: "/").map(String.init)
323
+ guard components.count == 3, components[0] == "Contents",
324
+ let originalMode = originalModes[relativePath]
325
+ else {
326
+ throw ApplicationNamespaceError.invalidBinding
327
+ }
328
+ let contents = try openSafeDirectoryAt(parent: application, leaf: components[0])
329
+ defer { Darwin.close(contents) }
330
+ let executableDirectory = try openSafeDirectoryAt(
331
+ parent: contents,
332
+ leaf: components[1]
333
+ )
334
+ defer { Darwin.close(executableDirectory) }
335
+ try setRegularFileMode(
336
+ parent: executableDirectory,
337
+ leaf: components[2],
338
+ allowedCurrentModes: [0, mode_t(originalMode)],
339
+ newMode: engageBarrier ? 0 : mode_t(originalMode)
340
+ )
341
+ }
342
+ }
343
+
344
+ private func openSafeDirectoryAt(parent: Int32, leaf: String) throws -> Int32 {
345
+ let descriptor = leaf.withCString {
346
+ openat(parent, $0, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
347
+ }
348
+ guard descriptor >= 0 else { throw ApplicationNamespaceError.invalidBinding }
349
+ var metadata = stat()
350
+ guard fstat(descriptor, &metadata) == 0,
351
+ (metadata.st_mode & S_IFMT) == S_IFDIR,
352
+ metadata.st_uid == 0,
353
+ (metadata.st_mode & 0o022) == 0,
354
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
355
+ else {
356
+ Darwin.close(descriptor)
357
+ throw ApplicationNamespaceError.invalidBinding
358
+ }
359
+ return descriptor
360
+ }
361
+
362
+ private func setRegularFileMode(
363
+ parent: Int32,
364
+ leaf: String,
365
+ allowedCurrentModes: Set<mode_t>,
366
+ newMode: mode_t
367
+ ) throws {
368
+ var namedBefore = stat()
369
+ let inspectStatus = leaf.withCString {
370
+ fstatat(parent, $0, &namedBefore, AT_SYMLINK_NOFOLLOW)
371
+ }
372
+ guard inspectStatus == 0,
373
+ (namedBefore.st_mode & S_IFMT) == S_IFREG,
374
+ namedBefore.st_uid == 0,
375
+ namedBefore.st_nlink == 1,
376
+ allowedCurrentModes.contains(namedBefore.st_mode & 0o777)
377
+ else {
378
+ throw ApplicationNamespaceError.invalidBinding
379
+ }
380
+ let descriptor = leaf.withCString {
381
+ openat(parent, $0, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
382
+ }
383
+ guard descriptor >= 0 else { throw ApplicationNamespaceError.invalidBinding }
384
+ defer { Darwin.close(descriptor) }
385
+ var opened = stat()
386
+ guard fstat(descriptor, &opened) == 0,
387
+ opened.st_dev == namedBefore.st_dev,
388
+ opened.st_ino == namedBefore.st_ino,
389
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor),
390
+ fchmod(descriptor, newMode) == 0,
391
+ fsync(descriptor) == 0
392
+ else {
393
+ throw ApplicationNamespaceError.mutationFailed
394
+ }
395
+ var namedAfter = stat()
396
+ let recheckStatus = leaf.withCString {
397
+ fstatat(parent, $0, &namedAfter, AT_SYMLINK_NOFOLLOW)
398
+ }
399
+ guard recheckStatus == 0,
400
+ namedAfter.st_dev == opened.st_dev,
401
+ namedAfter.st_ino == opened.st_ino,
402
+ (namedAfter.st_mode & 0o777) == newMode,
403
+ fsync(parent) == 0
404
+ else {
405
+ throw ApplicationNamespaceError.mutationFailed
406
+ }
407
+ }
408
+
409
+ private func directoryExists(parent: Int32, leaf: String) throws -> Bool {
410
+ var metadata = stat()
411
+ let status = leaf.withCString {
412
+ fstatat(parent, $0, &metadata, AT_SYMLINK_NOFOLLOW)
413
+ }
414
+ if status != 0, errno == ENOENT { return false }
415
+ guard status == 0, (metadata.st_mode & S_IFMT) == S_IFDIR else {
416
+ throw ApplicationNamespaceError.invalidBinding
417
+ }
418
+ return true
419
+ }
420
+
421
+ private static func openValidatedDirectory(
422
+ _ path: String,
423
+ exactApplicationsDirectory: Bool
424
+ ) throws -> Int32 {
425
+ guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(to: path) else {
426
+ throw ApplicationNamespaceError.unsafeDirectory
427
+ }
428
+ let descriptor = Darwin.open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
429
+ guard descriptor >= 0 else { throw ApplicationNamespaceError.unsafeDirectory }
430
+ let valid: Bool
431
+ if exactApplicationsDirectory {
432
+ valid = DarwinACLValidator.descriptorIsSafeRootOwnedDirectory(
433
+ descriptor,
434
+ exactPath: "/Applications"
435
+ )
436
+ } else {
437
+ var metadata = stat()
438
+ valid = fstat(descriptor, &metadata) == 0 &&
439
+ (metadata.st_mode & S_IFMT) == S_IFDIR &&
440
+ metadata.st_uid == 0 &&
441
+ (metadata.st_mode & 0o077) == 0 &&
442
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
443
+ }
444
+ guard valid else {
445
+ Darwin.close(descriptor)
446
+ throw ApplicationNamespaceError.unsafeDirectory
447
+ }
448
+ return descriptor
449
+ }
450
+ }
451
+
452
+ private extension String {
453
+ var deletingLastPathComponent: String {
454
+ URL(fileURLWithPath: self).deletingLastPathComponent().path
455
+ }
456
+ }
457
+
458
+ enum ApplicationNamespaceError: Error {
459
+ case invalidBinding
460
+ case unsafeDirectory
461
+ case mutationFailed
462
+ case couldNotSync
463
+ }
@@ -0,0 +1,55 @@
1
+ import Darwin
2
+ import Foundation
3
+ import RecordingsUpdateProtocol
4
+
5
+ enum ApplicationProcessQuiescence {
6
+ /// Reject activation while any executable loaded from the live bundle is still
7
+ /// running. The one authenticated update client servicing this XPC request is
8
+ /// exempt only when its kernel-reported executable path is the fixed client path.
9
+ static func requireQuiescence(excludingAuthenticatedClientPID excludedPID: pid_t?) throws {
10
+ let capacity = 16_384
11
+ var processIdentifiers = [pid_t](repeating: 0, count: capacity)
12
+ let count = processIdentifiers.withUnsafeMutableBytes { bytes in
13
+ proc_listallpids(bytes.baseAddress, Int32(bytes.count))
14
+ }
15
+ guard count >= 0, Int(count) < capacity else {
16
+ throw ApplicationProcessQuiescenceError.enumerationFailed
17
+ }
18
+
19
+ let liveContentsPrefix = RecordingsUpdateConstants.applicationPath + "/Contents/"
20
+ let authenticatedClientPath = RecordingsUpdateConstants.applicationPath + "/"
21
+ + RecordingsUpdateConstants.updateClientRelativePath
22
+ for processIdentifier in processIdentifiers.prefix(Int(count)) where processIdentifier > 0 {
23
+ var pathBytes = [CChar](repeating: 0, count: Int(PROC_PIDPATHINFO_MAXSIZE))
24
+ let pathLength = pathBytes.withUnsafeMutableBufferPointer { buffer in
25
+ proc_pidpath(
26
+ processIdentifier,
27
+ buffer.baseAddress,
28
+ UInt32(buffer.count)
29
+ )
30
+ }
31
+ if pathLength <= 0 {
32
+ // PIDs can disappear between enumeration and inspection. A live PID
33
+ // that cannot be inspected remains observable with kill(0), so only
34
+ // fail closed for that case.
35
+ if kill(processIdentifier, 0) == 0 || errno == EPERM {
36
+ throw ApplicationProcessQuiescenceError.processInspectionFailed
37
+ }
38
+ continue
39
+ }
40
+ let executablePath = String(cString: pathBytes)
41
+ guard executablePath.hasPrefix(liveContentsPrefix) else { continue }
42
+ if processIdentifier == excludedPID,
43
+ executablePath == authenticatedClientPath {
44
+ continue
45
+ }
46
+ throw ApplicationProcessQuiescenceError.liveBundleProcessRunning
47
+ }
48
+ }
49
+ }
50
+
51
+ enum ApplicationProcessQuiescenceError: Error {
52
+ case enumerationFailed
53
+ case processInspectionFailed
54
+ case liveBundleProcessRunning
55
+ }