diff --git a/Packages/OsaurusCore/Managers/Model/ModelManager.swift b/Packages/OsaurusCore/Managers/Model/ModelManager.swift index c87d515f6f..dc65956958 100644 --- a/Packages/OsaurusCore/Managers/Model/ModelManager.swift +++ b/Packages/OsaurusCore/Managers/Model/ModelManager.swift @@ -188,7 +188,27 @@ final class ModelManager: NSObject, ObservableObject { // Pull the OsaurusAI HF org listing once on launch so newly published // models surface in the Recommended tab without requiring a code push. - Task { [weak self] in await self?.loadOsaurusAIOrgModels() } + // + // The unit-test runner constructs `ModelManager()` repeatedly to drive + // `applyOsaurusOrgFetch` directly. If the launch-time HF fetch races + // with those test calls, whichever finishes last wins and the merge + // result is non-deterministic — that's the regression class behind + // `ModelManagerSuggestedTests/applyOsaurusOrgFetch_*` flaking in CI. + // Skip the background fetch under XCTest; production launches still + // get it because `XCTestConfigurationFilePath` is only set inside + // a test host. + if !Self.isRunningInTestEnvironment { + Task { [weak self] in await self?.loadOsaurusAIOrgModels() } + } + } + + /// True when the current process was launched by xctest. Used to gate + /// network-touching launch-time side effects so tests can drive the + /// affected code paths deterministically. + nonisolated private static var isRunningInTestEnvironment: Bool { + ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + || ProcessInfo.processInfo.environment["XCTestBundlePath"] != nil + || ProcessInfo.processInfo.environment["XCTestSessionIdentifier"] != nil } // MARK: - Public Methods diff --git a/Packages/OsaurusCore/Networking/HTTPHandler.swift b/Packages/OsaurusCore/Networking/HTTPHandler.swift index 0f88775490..147f16f5d4 100644 --- a/Packages/OsaurusCore/Networking/HTTPHandler.swift +++ b/Packages/OsaurusCore/Networking/HTTPHandler.swift @@ -1514,6 +1514,11 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { let isPermanent: Bool } + /// Placeholder written to the Insights log in place of a freshly-minted + /// API key. Surfaces clearly in the UI so operators know a redaction + /// happened, while being inert as a credential. + fileprivate static let redactedAPIKeyPlaceholder = "osk-v1-***REDACTED***" + /// POST /pair — unauthenticated endpoint for cryptographic Bonjour pairing. private func handlePairEndpoint( head: HTTPRequestHead, @@ -1708,6 +1713,20 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { (try? JSONEncoder().encode(response)).map { String(decoding: $0, as: UTF8.self) } ?? #"{"error":"Encoding failed"}"# + // Build a redacted copy for Insights: the full osk-v1 key is a + // bearer secret that grants master-scoped API access. It must + // never sit in the in-app log ring buffer, where it could leak + // via screen recordings, support-bundle exports, or memory + // dumps. The wire response is unchanged. + let redactedResponse = PairResponse( + agentAddress: agentAddress, + apiKey: Self.redactedAPIKeyPlaceholder, + isPermanent: isPermanent + ) + let redactedJson = + (try? JSONEncoder().encode(redactedResponse)).map { String(decoding: $0, as: UTF8.self) } + ?? #"{"redacted":true}"# + hop { var headers = [("Content-Type", "application/json; charset=utf-8")] headers.append(contentsOf: cors) @@ -1717,7 +1736,7 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { path: "/pair", userAgent: logUserAgent, requestBody: logRequestBody, - responseBody: json, + responseBody: redactedJson, responseStatus: 200, startTime: logStartTime )