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