Merge nucleic/upbeat-slate-lemur-7euo into dev
This commit is contained in:
@@ -437,7 +437,7 @@ struct TranscriptRow: View, Equatable {
|
||||
|
||||
/// Human-readable tool-result text: a plain string, the joined text of an MCP
|
||||
/// content array, or the canonical JSON as a last resort.
|
||||
static func resultText(_ value: JSONValue) -> String {
|
||||
nonisolated static func resultText(_ value: JSONValue) -> String {
|
||||
if let string = value.stringValue { return string }
|
||||
if let array = value.arrayValue {
|
||||
let texts = array.compactMap { $0["text"]?.stringValue }
|
||||
@@ -457,7 +457,7 @@ struct TranscriptRow: View, Equatable {
|
||||
/// delivers. (Sole divergence: a hand-built NaN/Infinity number, whose encode failure
|
||||
/// `canonicalString()` collapses to "null"; JSON can't carry one, and the expanded
|
||||
/// branches re-check `resultText` anyway, so a false promise degrades to an empty body.)
|
||||
static func resultHasText(_ value: JSONValue) -> Bool {
|
||||
nonisolated static func resultHasText(_ value: JSONValue) -> Bool {
|
||||
if let string = value.stringValue { return !string.isEmpty }
|
||||
if let array = value.arrayValue {
|
||||
let texts = array.compactMap { $0["text"]?.stringValue }
|
||||
|
||||
@@ -4232,7 +4232,7 @@ public final class AppStore: ConflictArbiter {
|
||||
if Task.isCancelled { break }
|
||||
if await self.runNvrsionSweep() { break } // nothing left warm → park
|
||||
}
|
||||
await self?.clearNvrsionKeepWarmTask()
|
||||
self?.clearNvrsionKeepWarmTask()
|
||||
}
|
||||
}
|
||||
private func clearNvrsionKeepWarmTask() { nvrsionKeepWarmTask = nil }
|
||||
|
||||
@@ -1523,7 +1523,7 @@ public actor MCPApprovalServer {
|
||||
await prior?.value
|
||||
try? await self.sendResponse(response, on: conn)
|
||||
}
|
||||
await self.handlerTaskFinished(taskID, token: token)
|
||||
self.handlerTaskFinished(taskID, token: token)
|
||||
}
|
||||
previous = task
|
||||
spawned.append(taskID)
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
// `SecKeychainGet/SetUserInteractionAllowed` are formally deprecated but remain the ONLY API that
|
||||
// suppresses the legacy Keychain ACL/partition-list password panel (the data-protection
|
||||
// `kSecUseAuthenticationUI*` flags govern only Touch ID / passcode presence, not that dialog). Bind
|
||||
// the C symbols directly — the deprecation rides on their Swift imports, not the raw symbols — so
|
||||
// the required calls in `withoutLegacyKeychainUI` compile without deprecation warnings.
|
||||
@_silgen_name("SecKeychainGetUserInteractionAllowed")
|
||||
private func nucleic_SecKeychainGetUserInteractionAllowed(_ state: UnsafeMutablePointer<DarwinBoolean>) -> OSStatus
|
||||
@_silgen_name("SecKeychainSetUserInteractionAllowed")
|
||||
private func nucleic_SecKeychainSetUserInteractionAllowed(_ state: DarwinBoolean) -> OSStatus
|
||||
|
||||
/// Shared persistence for Nucleic's own secrets that is structurally incapable of presenting a
|
||||
/// Keychain authorization panel.
|
||||
///
|
||||
@@ -93,14 +103,15 @@ enum KeychainOwnedAccess {
|
||||
}
|
||||
|
||||
/// Run `body` with the legacy Keychain's interactive authorization panel disabled, restoring the
|
||||
/// prior setting afterward. `SecKeychain*` is formally deprecated (its three uses below emit
|
||||
/// expected deprecation warnings) but remains the ONLY API that suppresses that panel — its
|
||||
/// data-protection successors don't govern the legacy ACL dialog at all.
|
||||
/// prior setting afterward. `SecKeychain*` is formally deprecated but remains the ONLY API that
|
||||
/// suppresses that panel — its data-protection successors don't govern the legacy ACL dialog at
|
||||
/// all. The three symbols are bound directly (`nucleic_SecKeychain*`, top of file) so the
|
||||
/// required calls compile without deprecation warnings.
|
||||
private static func withoutLegacyKeychainUI<T>(_ body: () -> T) -> T {
|
||||
var previous = DarwinBoolean(true)
|
||||
SecKeychainGetUserInteractionAllowed(&previous)
|
||||
SecKeychainSetUserInteractionAllowed(false)
|
||||
defer { SecKeychainSetUserInteractionAllowed(previous.boolValue) }
|
||||
_ = nucleic_SecKeychainGetUserInteractionAllowed(&previous)
|
||||
_ = nucleic_SecKeychainSetUserInteractionAllowed(false)
|
||||
defer { _ = nucleic_SecKeychainSetUserInteractionAllowed(previous) }
|
||||
return body()
|
||||
}
|
||||
|
||||
|
||||
@@ -343,7 +343,7 @@ public enum ContainerServiceSettings {
|
||||
@TaskLocal private static var defaultsBox = DefaultsBox(defaults: .standard)
|
||||
|
||||
/// The store every accessor reads and `enableService()` writes. Production always resolves
|
||||
/// to `.standard`; tests bind a fresh isolated suite via ``withDefaults(_:isolation:operation:)``
|
||||
/// to `.standard`; tests bind a fresh isolated suite via ``withDefaults(_:operation:)``
|
||||
/// so they can flip these switches without ever mutating the process-global domain. Task-local
|
||||
/// because swift-testing runs suites in parallel: a global (even save/restored) override of
|
||||
/// `serviceEnabledKey` is visible to every concurrently running test, and a leaked `true`
|
||||
@@ -357,11 +357,10 @@ public enum ContainerServiceSettings {
|
||||
/// invisible to concurrent tasks — the isolation seam tests bind a per-test suite through.
|
||||
public static func withDefaults<R>(
|
||||
_ store: UserDefaults,
|
||||
isolation: isolated (any Actor)? = #isolation,
|
||||
operation: () async throws -> R
|
||||
operation: nonisolated(nonsending) () async throws -> R
|
||||
) async rethrows -> R {
|
||||
try await $defaultsBox.withValue(
|
||||
DefaultsBox(defaults: store), operation: operation, isolation: isolation)
|
||||
DefaultsBox(defaults: store), operation: operation)
|
||||
}
|
||||
|
||||
/// Master switch. Off → the per-project "run sessions in a sandbox container" option is
|
||||
|
||||
@@ -210,8 +210,6 @@ public actor SessionTransferImporter {
|
||||
entry.staged = true
|
||||
try await store.updateTransferState(transferID: chunk.transferID, to: .ready, at: now())
|
||||
return [ack, .transferReady(chunk.transferID)]
|
||||
} catch let reject as TransferReject {
|
||||
return [ack] + failStaging(entry, reject.reason, reject.message)
|
||||
} catch {
|
||||
return [ack] + failStaging(entry, .internalError, "Staging failed: \(error)")
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ enum DERBuilder {
|
||||
static func constructed(_ id: ASN1Identifier, _ children: [[UInt8]]) -> [UInt8] {
|
||||
var s = DER.Serializer()
|
||||
// appendConstructedNode never throws for raw-byte writes.
|
||||
try! s.appendConstructedNode(identifier: id) { inner in
|
||||
s.appendConstructedNode(identifier: id) { inner in
|
||||
for c in children { inner.serializeRawBytes(c) }
|
||||
}
|
||||
return s.serializedBytes
|
||||
@@ -53,7 +53,7 @@ enum DERBuilder {
|
||||
|
||||
static func primitive(_ id: ASN1Identifier, _ write: (inout [UInt8]) -> Void) -> [UInt8] {
|
||||
var s = DER.Serializer()
|
||||
try! s.appendPrimitiveNode(identifier: id) { write(&$0) }
|
||||
s.appendPrimitiveNode(identifier: id) { write(&$0) }
|
||||
return s.serializedBytes
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
|
||||
/// Temporary opt-in tracing for the direct-upgrade handshake (set NUCLEIC_DIRECT_DEBUG=1).
|
||||
enum DirectDebug {
|
||||
nonisolated(unsafe) static let on = ProcessInfo.processInfo.environment["NUCLEIC_DIRECT_DEBUG"] == "1"
|
||||
static let on = ProcessInfo.processInfo.environment["NUCLEIC_DIRECT_DEBUG"] == "1"
|
||||
static func log(_ s: String) {
|
||||
if on { FileHandle.standardError.write(Data("DIRECT: \(s)\n".utf8)) }
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ struct AppStoreSyncBridgeTests {
|
||||
let repo = try await GitTestRepo()
|
||||
defer { repo.cleanup() }
|
||||
let store = makeStore(repo: repo)
|
||||
#expect(await store.capabilities.canSyncTranscripts == true)
|
||||
#expect(store.capabilities.canSyncTranscripts == true)
|
||||
}
|
||||
|
||||
@Test func transcriptEventsServesFullHistoryAndHeader() async throws {
|
||||
@@ -131,7 +131,7 @@ struct AppStoreSyncBridgeTests {
|
||||
let repo = try await GitTestRepo()
|
||||
defer { repo.cleanup() }
|
||||
let store = makeStore(repo: repo)
|
||||
#expect(await store.capabilities.canReceiveSessionTransfer == true)
|
||||
#expect(store.capabilities.canReceiveSessionTransfer == true)
|
||||
}
|
||||
|
||||
@Test func offerForUnknownProjectIsRejected() async throws {
|
||||
|
||||
@@ -2369,7 +2369,7 @@ struct AppStoreTests {
|
||||
let store = makeStore(repo: repo)
|
||||
let project = await store.addProject(name: "demo", rootPath: repo.root, defaultBranch: "main")!
|
||||
|
||||
let sessionID = try await store.newSession(in: project)!
|
||||
let sessionID = await store.newSession(in: project)!
|
||||
await store.renameOpenSession(to: "my-cool-chat")
|
||||
#expect(store.openSession?.title == "my-cool-chat")
|
||||
#expect(store.summaries.first { $0.id == sessionID }?.title == "my-cool-chat")
|
||||
@@ -2383,7 +2383,7 @@ struct AppStoreTests {
|
||||
defer { repo.cleanup() }
|
||||
let store = makeStore(repo: repo)
|
||||
let project = await store.addProject(name: "demo", rootPath: repo.root, defaultBranch: "main")!
|
||||
let sessionID = try await store.newSession(in: project)!
|
||||
let sessionID = await store.newSession(in: project)!
|
||||
|
||||
await store.setOpenSessionModel("opus")
|
||||
await store.setOpenSessionEffort("high")
|
||||
@@ -2878,7 +2878,7 @@ struct AppStoreTests {
|
||||
store.defaultAuto = true
|
||||
let project = await store.addProject(name: "demo", rootPath: repo.root, defaultBranch: "main")!
|
||||
|
||||
_ = try await store.newSession(in: project)
|
||||
_ = await store.newSession(in: project)
|
||||
await waitFor { store.openSession != nil }
|
||||
#expect(store.openSession?.model == "claude-sonnet-4-6")
|
||||
#expect(store.openSession?.effort == "low")
|
||||
|
||||
@@ -274,7 +274,7 @@ import Testing
|
||||
|
||||
@Test func restoreImageURLSettingReads() async throws {
|
||||
let suite = UserDefaults(suiteName: "macvm-url-\(UUID().uuidString)")!
|
||||
try await ContainerServiceSettings.withDefaults(suite) {
|
||||
await ContainerServiceSettings.withDefaults(suite) {
|
||||
#expect(MacVMSettings.restoreImageURL == nil)
|
||||
suite.set("https://example.com/UniversalMac_27.0.ipsw", forKey: MacVMSettings.restoreImageURLKey)
|
||||
#expect(MacVMSettings.restoreImageURL == "https://example.com/UniversalMac_27.0.ipsw")
|
||||
@@ -381,7 +381,7 @@ import Testing
|
||||
|
||||
@Test func pendingBaseCarryPersistsSetAndClears() async throws {
|
||||
let suite = UserDefaults(suiteName: "macvm-carry-\(UUID().uuidString)")!
|
||||
try await ContainerServiceSettings.withDefaults(suite) {
|
||||
await ContainerServiceSettings.withDefaults(suite) {
|
||||
#expect(MacVMSettings.pendingBaseCarryApps.isEmpty)
|
||||
#expect(MacVMSettings.pendingBaseCarryPackages.isEmpty)
|
||||
// A base being deleted stashes its recorded set for the next build to restore.
|
||||
@@ -508,7 +508,7 @@ import Testing
|
||||
|
||||
@Test func settingsDefaultsAndOverrides() async throws {
|
||||
let suite = UserDefaults(suiteName: "macvm-test-\(UUID().uuidString)")!
|
||||
try await ContainerServiceSettings.withDefaults(suite) {
|
||||
await ContainerServiceSettings.withDefaults(suite) {
|
||||
// Defaults with an empty suite.
|
||||
#expect(MacVMSettings.serviceEnabled == false)
|
||||
#expect(MacVMSettings.exposeByDefault == false) // false while service is off
|
||||
@@ -981,7 +981,7 @@ import Testing
|
||||
|
||||
@Test func agentPasswordSettingReads() async throws {
|
||||
let suite = UserDefaults(suiteName: "macvm-pw-\(UUID().uuidString)")!
|
||||
try await ContainerServiceSettings.withDefaults(suite) {
|
||||
await ContainerServiceSettings.withDefaults(suite) {
|
||||
#expect(MacVMSettings.agentPassword == nil)
|
||||
suite.set(" hunter2 ", forKey: MacVMSettings.agentPasswordKey)
|
||||
#expect(MacVMSettings.agentPassword == "hunter2")
|
||||
@@ -999,7 +999,7 @@ import Testing
|
||||
|
||||
@Test func computerUseSettingGatedOnService() async throws {
|
||||
let suite = UserDefaults(suiteName: "macvm-cu-\(UUID().uuidString)")!
|
||||
try await ContainerServiceSettings.withDefaults(suite) {
|
||||
await ContainerServiceSettings.withDefaults(suite) {
|
||||
suite.set(true, forKey: MacVMSettings.computerUseByDefaultKey)
|
||||
#expect(MacVMSettings.computerUseByDefault == false) // service off ⇒ false
|
||||
suite.set(true, forKey: MacVMSettings.serviceEnabledKey)
|
||||
@@ -1066,7 +1066,7 @@ import Testing
|
||||
|
||||
@Test func linuxSettingsDefaultsAndOverrides() async throws {
|
||||
let suite = UserDefaults(suiteName: "linuxvm-test-\(UUID().uuidString)")!
|
||||
try await ContainerServiceSettings.withDefaults(suite) {
|
||||
await ContainerServiceSettings.withDefaults(suite) {
|
||||
#expect(MacVMSettings.linuxServiceEnabled == false)
|
||||
#expect(MacVMSettings.linuxExposeByDefault == false) // false while service off
|
||||
#expect(MacVMSettings.linuxBasePrebuiltPath == nil)
|
||||
@@ -1139,7 +1139,7 @@ import Testing
|
||||
|
||||
@Test func selectedPackageIDsPersistAndFilterOutUnavailable() async throws {
|
||||
let suite = UserDefaults(suiteName: "macvm-pkgs-\(UUID().uuidString)")!
|
||||
try await ContainerServiceSettings.withDefaults(suite) {
|
||||
await ContainerServiceSettings.withDefaults(suite) {
|
||||
#expect(MacVMSettings.selectedPackageIDs.isEmpty)
|
||||
// A grayed-out (xcode) or unknown id can never be selected — only installable ids survive.
|
||||
MacVMSettings.setSelectedPackageIDs(["chrome", "xcode", "bogus"])
|
||||
|
||||
@@ -229,7 +229,7 @@ import NucleicProtocol
|
||||
let (host, _, client, recorder, _) = try await makePaired(bridge: bridge)
|
||||
_ = await recorder.waitFor { if case .ready = $0 { return true } else { return false } }
|
||||
// The host advertises the capability so a real client would know it may ask.
|
||||
#expect(await bridge.capabilities.canListPeers == false) // FakeSyncBridge default; real host sets true
|
||||
#expect(bridge.capabilities.canListPeers == false) // FakeSyncBridge default; real host sets true
|
||||
|
||||
await client.send(.listPeers)
|
||||
let event = await recorder.waitFor { if case .peerList = $0 { return true } else { return false } }
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
@testable import NucleicProtocol
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ import Testing
|
||||
|
||||
/// A host that predates `canListPeers` decodes it as false (client won't send `listPeers`).
|
||||
@Test func capabilitiesTolerateMissingCanListPeers() throws {
|
||||
struct LegacyCaps: Codable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
struct LegacyCaps: Encodable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
let data = try CBOREncoder().encode(LegacyCaps())
|
||||
let decoded = try CBORDecoder().decode(WireCapabilities.self, from: data)
|
||||
#expect(decoded.canListPeers == false)
|
||||
@@ -135,7 +135,7 @@ import Testing
|
||||
/// A host that predates `canMintPairingCode` decodes it as false (the phone hides the
|
||||
/// "add a device" button and never sends `requestPairingCode`).
|
||||
@Test func capabilitiesTolerateMissingCanMintPairingCode() throws {
|
||||
struct LegacyCaps: Codable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
struct LegacyCaps: Encodable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
let data = try CBOREncoder().encode(LegacyCaps())
|
||||
let decoded = try CBORDecoder().decode(WireCapabilities.self, from: data)
|
||||
#expect(decoded.canMintPairingCode == false)
|
||||
@@ -226,10 +226,10 @@ import Testing
|
||||
/// A host/client that predates `canSyncRoster` decodes it as false → falls back to the
|
||||
/// display-only `listPeers` path, never sending/expecting `meshRoster`.
|
||||
@Test func capabilitiesTolerateMissingCanSyncRoster() throws {
|
||||
struct LegacyCaps: Codable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
struct LegacyCaps: Encodable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
let decoded = try CBORDecoder().decode(WireCapabilities.self, from: CBOREncoder().encode(LegacyCaps()))
|
||||
#expect(decoded.canSyncRoster == false)
|
||||
struct LegacyClientCaps: Codable { let mesh = 1 }
|
||||
struct LegacyClientCaps: Encodable { let mesh = 1 }
|
||||
let cc = try CBORDecoder().decode(
|
||||
WireClientCapabilities.self, from: CBOREncoder().encode(LegacyClientCaps()))
|
||||
#expect(cc.canSyncRoster == false)
|
||||
@@ -278,7 +278,7 @@ import Testing
|
||||
/// A host that predates `canSyncSettings` decodes it as false → the client never sends
|
||||
/// `updateSettings` and never expects `HostMsg.settings`.
|
||||
@Test func capabilitiesTolerateMissingCanSyncSettings() throws {
|
||||
struct LegacyCaps: Codable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
struct LegacyCaps: Encodable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
let decoded = try CBORDecoder().decode(WireCapabilities.self, from: CBOREncoder().encode(LegacyCaps()))
|
||||
#expect(decoded.canSyncSettings == false)
|
||||
}
|
||||
@@ -328,10 +328,10 @@ import Testing
|
||||
/// direct verb at them (old hosts *error* on unknown ClientMsg tags, so this gate is
|
||||
/// what keeps the upgrade invisible to them).
|
||||
@Test func capabilitiesTolerateMissingCanDirectConnect() throws {
|
||||
struct LegacyCaps: Codable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
struct LegacyCaps: Encodable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
let host = try CBORDecoder().decode(WireCapabilities.self, from: CBOREncoder().encode(LegacyCaps()))
|
||||
#expect(host.canDirectConnect == false)
|
||||
struct LegacyClientCaps: Codable { let mesh = 1 }
|
||||
struct LegacyClientCaps: Encodable { let mesh = 1 }
|
||||
let client = try CBORDecoder().decode(
|
||||
WireClientCapabilities.self, from: CBOREncoder().encode(LegacyClientCaps()))
|
||||
#expect(client.canDirectConnect == false)
|
||||
@@ -401,7 +401,7 @@ import Testing
|
||||
|
||||
/// A host that predates `canUpdateAddresses` decodes it as false (client never sends it).
|
||||
@Test func capabilitiesTolerateMissingCanUpdateAddresses() throws {
|
||||
struct LegacyCaps: Codable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
struct LegacyCaps: Encodable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
let decoded = try CBORDecoder().decode(
|
||||
WireCapabilities.self, from: CBOREncoder().encode(LegacyCaps()))
|
||||
#expect(decoded.canUpdateAddresses == false)
|
||||
@@ -467,7 +467,7 @@ import Testing
|
||||
StartChatRequest(projectID: ProjectID(rawValue: "p1"), message: "go")))
|
||||
#expect(legacy.covalenceOriginDeviceID == nil)
|
||||
// A pre-rename sender using the old "carbonOriginDeviceID" key — still read via fallback.
|
||||
struct LegacyStart: Codable {
|
||||
struct LegacyStart: Encodable {
|
||||
let projectID = "p1", message = "go"
|
||||
let useWorktree = true
|
||||
let carbonOriginDeviceID = "origin-legacy"
|
||||
@@ -493,7 +493,7 @@ import Testing
|
||||
guard case .transferOffer(let decoded) = carried else { Issue.record("no offer"); return }
|
||||
#expect(decoded.record.covalenceOriginDeviceID == "origin-phone")
|
||||
// A pre-Covalence source's record (no key) decodes nil — the session arrives unmanaged.
|
||||
struct LegacyRecord: Codable {
|
||||
struct LegacyRecord: Encodable {
|
||||
let sessionID = "s1", backend = "claudeCode", title = "t", branch = "nucleic/t"
|
||||
let baseSHA = "b"
|
||||
let lastSeq: UInt64 = 1
|
||||
@@ -503,7 +503,7 @@ import Testing
|
||||
SessionTransferRecord.self, from: CBOREncoder().encode(LegacyRecord()))
|
||||
#expect(legacy.covalenceOriginDeviceID == nil)
|
||||
// A pre-rename source using the old "carbonOriginDeviceID" key — read via fallback.
|
||||
struct LegacyManagedRecord: Codable {
|
||||
struct LegacyManagedRecord: Encodable {
|
||||
let sessionID = "s1", backend = "claudeCode", title = "t", branch = "nucleic/t"
|
||||
let baseSHA = "b"
|
||||
let lastSeq: UInt64 = 1
|
||||
@@ -523,7 +523,7 @@ import Testing
|
||||
guard case .transferAccept(let decoded) = carried else { Issue.record("no accept"); return }
|
||||
#expect(decoded.alreadyHaveItems == [.transcript])
|
||||
// A pre-mirror destination's accept (no key) decodes empty — the source streams all.
|
||||
struct LegacyAccept: Codable {
|
||||
struct LegacyAccept: Encodable {
|
||||
let transferID = "x", resolvedProjectID = "p"
|
||||
let haveSHAs: [String] = []
|
||||
}
|
||||
@@ -549,7 +549,7 @@ import Testing
|
||||
|
||||
/// A host that predates the transfer capability decodes it false — a source never offers.
|
||||
@Test func capabilitiesTolerateMissingCanReceiveSessionTransfer() throws {
|
||||
struct LegacyCaps: Codable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
struct LegacyCaps: Encodable { let canModifyToolInput = true; let allowAlwaysScopes: [AlwaysScope] = [] }
|
||||
let decoded = try CBORDecoder().decode(
|
||||
WireCapabilities.self, from: CBOREncoder().encode(LegacyCaps()))
|
||||
#expect(decoded.canReceiveSessionTransfer == false)
|
||||
|
||||
+13
-2
@@ -116,8 +116,19 @@ in-tree means the patch can't be lost to a dependency re-resolve.
|
||||
(the `cctl login` write path) gains a delete-and-retry on `errSecDuplicateItem`, since the
|
||||
now-silent `exists` can under-report an unreadable pre-existing item. Mirrors
|
||||
`KeychainOwnedAccess.withoutLegacyKeychainUI` in NucleicCore. Host-side (shipped by a normal
|
||||
`swift build`); `SecKeychain*` deprecation warnings are expected (built with
|
||||
`WARNINGS_AS_ERRORS=false`). Marked `[Nucleic vendored patch]`.
|
||||
`swift build`). The three `SecKeychain*` symbols are formally deprecated but are the only API
|
||||
covering the legacy ACL panel; they're bound directly via `@_silgen_name` (`nucleic_SecKeychain*`,
|
||||
top of file) so the required calls compile without deprecation warnings. Marked
|
||||
`[Nucleic vendored patch]`.
|
||||
|
||||
14. **`Sources/Containerization/Vminitd.swift` — configure the gRPC pipeline before the channel goes
|
||||
active.** `Vminitd.init` used the now-deprecated `HTTP2ClientTransport.WrappedChannel.wrapping(
|
||||
channel:config:serviceConfig:)`, which wraps an already-connected channel best-effort and may drop
|
||||
early server frames such as SETTINGS. Migrated to `wrapping(config:serviceConfig:makeChannel:)`,
|
||||
which invokes the transport's `configure` inside the bootstrap's channel initializer — before the
|
||||
vsock channel becomes active. `init` is now `async throws` (the new overload is async); its callers
|
||||
in `VZVirtualMachineInstance` (`start`/`dialAgent`, both already async) and the integration test now
|
||||
`try await`. Marked `[Nucleic vendored patch]`.
|
||||
|
||||
### GUEST-side patches (require rebuilding the initfs — see below)
|
||||
|
||||
|
||||
+2
-2
@@ -191,7 +191,7 @@ extension VZVirtualMachineInstance: VirtualMachineInstance {
|
||||
|
||||
try await self.vm.start(queue: self.queue)
|
||||
|
||||
let agent = try Vminitd(
|
||||
let agent = try await Vminitd(
|
||||
connection: try await self.vm.waitForAgent(queue: self.queue),
|
||||
group: self.group
|
||||
)
|
||||
@@ -260,7 +260,7 @@ extension VZVirtualMachineInstance: VirtualMachineInstance {
|
||||
port: Vminitd.port
|
||||
)
|
||||
let handle = try conn.dupHandle()
|
||||
return try Vminitd(connection: handle, group: self.group)
|
||||
return try await Vminitd(connection: handle, group: self.group)
|
||||
} catch {
|
||||
if let err = error as? ContainerizationError {
|
||||
throw err
|
||||
|
||||
@@ -34,18 +34,23 @@ public struct Vminitd: Sendable {
|
||||
public let grpcClient: GRPCClient<HTTP2ClientTransport.WrappedChannel>
|
||||
private let connectionTask: Task<Void, Error>
|
||||
|
||||
public init(connection: FileHandle, group: any EventLoopGroup) throws {
|
||||
let channel = try ClientBootstrap(group: group)
|
||||
.channelInitializer { channel in
|
||||
channel.eventLoop.makeCompletedFuture(withResultOf: {
|
||||
try channel.pipeline.syncOperations.addHandler(HTTP2ConnectBufferingHandler())
|
||||
})
|
||||
}
|
||||
.withConnectedSocket(connection.fileDescriptor).wait()
|
||||
let transport = HTTP2ClientTransport.WrappedChannel.wrapping(
|
||||
channel: channel,
|
||||
public init(connection: FileHandle, group: any EventLoopGroup) async throws {
|
||||
// Configure the gRPC pipeline from inside the channel initializer — before the channel
|
||||
// becomes active — so no early server frames (e.g. SETTINGS) are dropped. `configure` is
|
||||
// supplied by `wrapping(config:serviceConfig:makeChannel:)` and must be called exactly once.
|
||||
let fd = connection.fileDescriptor
|
||||
let transport = try await HTTP2ClientTransport.WrappedChannel.wrapping(
|
||||
config: .defaults { $0.connection.maxIdleTime = nil }
|
||||
)
|
||||
) { configure in
|
||||
try await ClientBootstrap(group: group)
|
||||
.withConnectedSocket(fd) { channel in
|
||||
channel.eventLoop.makeCompletedFuture {
|
||||
try channel.pipeline.syncOperations.addHandler(HTTP2ConnectBufferingHandler())
|
||||
}.flatMap { _ in
|
||||
configure(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
let grpcClient = GRPCClient(transport: transport)
|
||||
self.grpcClient = grpcClient
|
||||
self.client = Com_Apple_Containerization_Sandbox_V3_SandboxContext.Client(wrapping: self.grpcClient)
|
||||
|
||||
+12
-3
@@ -22,6 +22,15 @@ import Foundation
|
||||
#endif
|
||||
import Security // [Nucleic vendored patch] SecKeychain*UserInteractionAllowed for prompt-free reads
|
||||
|
||||
// [Nucleic vendored patch] `SecKeychainGet/SetUserInteractionAllowed` are formally deprecated but
|
||||
// remain the ONLY API that suppresses the legacy Keychain ACL panel. Bind the C symbols directly —
|
||||
// the deprecation rides on their Swift imports, not the raw symbols — so `withoutInteractiveUI`
|
||||
// compiles without deprecation warnings. Mirrors `KeychainOwnedAccess` in NucleicCore.
|
||||
@_silgen_name("SecKeychainGetUserInteractionAllowed")
|
||||
private func nucleic_SecKeychainGetUserInteractionAllowed(_ state: UnsafeMutablePointer<DarwinBoolean>) -> OSStatus
|
||||
@_silgen_name("SecKeychainSetUserInteractionAllowed")
|
||||
private func nucleic_SecKeychainSetUserInteractionAllowed(_ state: DarwinBoolean) -> OSStatus
|
||||
|
||||
/// Holds the result of a query to the keychain.
|
||||
public struct KeychainQueryResult {
|
||||
public var username: String
|
||||
@@ -260,9 +269,9 @@ public struct KeychainQuery {
|
||||
/// `KeychainOwnedAccess.withoutLegacyKeychainUI` in NucleicCore.
|
||||
private static func withoutInteractiveUI<T>(_ body: () -> T) -> T {
|
||||
var previous = DarwinBoolean(true)
|
||||
SecKeychainGetUserInteractionAllowed(&previous)
|
||||
SecKeychainSetUserInteractionAllowed(false)
|
||||
defer { SecKeychainSetUserInteractionAllowed(previous.boolValue) }
|
||||
_ = nucleic_SecKeychainGetUserInteractionAllowed(&previous)
|
||||
_ = nucleic_SecKeychainSetUserInteractionAllowed(false)
|
||||
defer { _ = nucleic_SecKeychainSetUserInteractionAllowed(previous) }
|
||||
return body()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1730,7 +1730,7 @@ extension IntegrationSuite {
|
||||
try await assertExec(container, id: "create-fifo", cmd: "mkfifo /tmp/test-fifo")
|
||||
|
||||
let vsock = try await container.dialVsock(port: 1024)
|
||||
let vminitd = try Vminitd(connection: vsock, group: Self.eventLoop)
|
||||
let vminitd = try await Vminitd(connection: vsock, group: Self.eventLoop)
|
||||
|
||||
let root = URL(filePath: container.root)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user