field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 7465b598a80fe63955e56a3b09d0afcfdb7e3b5b
parent 43121e80950668f9f942dd90483e749a2838053b
Author: triesap <tyson@radroots.org>
Date:   Fri,  2 Oct 2026 14:26:35 +0000

diagnostics: bound and sanitize support evidence

- Apply a closed diagnostic schema before every native sink
- Sanitize metadata and preserve bounded explicit support exports
- Share a private formatter across Rust file and stdout logs
- Qualify actual sink and export canaries at exact native inputs

Diffstat:
MPackage.swift | 1+
MTera.xcodeproj/project.pbxproj | 4++++
ATera/Runtime/TeraDiagnosticPolicy.swift | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTera/Runtime/TeraLifecycleCoordinator.swift | 36++++++++++++++++++------------------
MTeraFFI/provenance.json | 54+++++++++++++++++++++++++++---------------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 26+++++++++++++++++++-------
MTeraFFI/source/aarch64-apple-ios-sim.json | 26+++++++++++++++++++-------
MTeraFFI/source/aarch64-apple-ios.json | 26+++++++++++++++++++-------
ATeraTests/TeraDiagnosticPolicyTests.swift | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTeraTests/TeraLifecycleTests.swift | 35+++++++++++++++++++++++++++++------
Mcore/crates/tera_ffi/src/logging.rs | 14+++++++++-----
Acore/crates/tera_ffi/src/logging/privacy_format.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_ffi/tests/logging_privacy.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mproject.yml | 1+
Mrelease/provenance.json | 6+++---
Mtest-fixtures/legacy-identifiers.v1.json | 48+++++++++++++++++++++++++++++++++---------------
17 files changed, 516 insertions(+), 97 deletions(-)

diff --git a/Package.swift b/Package.swift @@ -100,6 +100,7 @@ let package = Package( "Runtime/TeraMediaProcessUse.swift", "Runtime/TeraRemoteQualificationMediaPicker.swift", "Runtime/TeraDiagnosticsStore.swift", + "Runtime/TeraDiagnosticPolicy.swift", "Runtime/TeraGeneratedRuntimeStartup.swift", "Runtime/TeraBackupRuntimeStartup.swift", "Runtime/TeraBackupFiles.swift", diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -118,6 +118,7 @@ C0C7D6F0CBCE262C348D3C3E /* TeraTodayReconciliationFFITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F65B01F6F5F7E125148EC4FD /* TeraTodayReconciliationFFITests.swift */; }; C20BDCF02F461EE53B14B53E /* TeraPermissionRecoveryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BF69ADD9540F6800FD64B0 /* TeraPermissionRecoveryTests.swift */; }; C219B5F4E24188E1116BF45B /* TeraRuntimeClientFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88ED360C84532DA7088BFC6E /* TeraRuntimeClientFixtures.swift */; }; + C2CEC638DDABB9D01DD93A72 /* TeraDiagnosticPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE158F5F1E490847A5196EB /* TeraDiagnosticPolicyTests.swift */; }; C3177E827FE62639579C3EF5 /* TeraRemoteQualificationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32CBDAE974FB8D7408108E69 /* TeraRemoteQualificationUITests.swift */; }; C344F7761A4AC79AB74705C9 /* TeraTodayCalendarScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2BE93A0818202625CAF1F5 /* TeraTodayCalendarScopeTests.swift */; }; C46F8EB5B196FFDD37E0279A /* TeraProductStartupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF60EF9DCFDC4ECE77AED67 /* TeraProductStartupTests.swift */; }; @@ -300,6 +301,7 @@ E62DD95FB04526E6E69AB3D8 /* TeraSubmissionFormTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSubmissionFormTests.swift; sourceTree = "<group>"; }; E75B60DBDD8C581D76E6F957 /* TeraScopedObservationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopedObservationTests.swift; sourceTree = "<group>"; }; ECE8A3704AB9582A5E461195 /* TeraDraftRecoveryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraDraftRecoveryTests.swift; sourceTree = "<group>"; }; + EDE158F5F1E490847A5196EB /* TeraDiagnosticPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraDiagnosticPolicyTests.swift; sourceTree = "<group>"; }; EE1A300DCBB15E1911EA7D6C /* TeraCoordinateAdmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraCoordinateAdmissionTests.swift; sourceTree = "<group>"; }; EFBAA57ABEAA4B84C73EE8E8 /* TeraScopedMediaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopedMediaTests.swift; sourceTree = "<group>"; }; EFECCBF9A84D1D65544C6094 /* TeraTodayContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayContextTests.swift; sourceTree = "<group>"; }; @@ -406,6 +408,7 @@ DE21EEF3B1A13F1B5A10A3BF /* TeraComposerStartupTests.swift */, 56A65AF934F7D75487094C79 /* TeraComposerSubmissionCaptureTests.swift */, EE1A300DCBB15E1911EA7D6C /* TeraCoordinateAdmissionTests.swift */, + EDE158F5F1E490847A5196EB /* TeraDiagnosticPolicyTests.swift */, ECE8A3704AB9582A5E461195 /* TeraDraftRecoveryTests.swift */, 22BEF874B8D521920E1D16B7 /* TeraDurableMediaRootsTests.swift */, AC9B19425D3374B899485C17 /* TeraEditingOperationProtectionTests.swift */, @@ -739,6 +742,7 @@ 3410EC660FF36CC77F5F50DD /* TeraComposerStartupTests.swift in Sources */, C73B145B6858220FFFA587B2 /* TeraComposerSubmissionCaptureTests.swift in Sources */, 207C19433F0386DD7D43A42D /* TeraCoordinateAdmissionTests.swift in Sources */, + C2CEC638DDABB9D01DD93A72 /* TeraDiagnosticPolicyTests.swift in Sources */, 64F5AD3749E090316C7D015E /* TeraDraftRecoveryTests.swift in Sources */, 80F3B531DAA8C2F6CE17D357 /* TeraDurableMediaRootsTests.swift in Sources */, F14217EA66C2A9397E36E3FA /* TeraEditingOperationProtectionTests.swift in Sources */, diff --git a/Tera/Runtime/TeraDiagnosticPolicy.swift b/Tera/Runtime/TeraDiagnosticPolicy.swift @@ -0,0 +1,75 @@ +import Foundation +import RadrootsKit + +/// An app-owned closed schema, applied before all sinks and again at the +/// standalone buffer boundary. Pattern redaction alone cannot admit content. +enum TeraDiagnosticPolicy { + static let eventNames: Set<String> = [ + "ios.lifecycle.start_requested", "ios.lifecycle.resume_coalesced", + "ios.lifecycle.active", "ios.lifecycle.background", + "ios.lifecycle.protected_data_available", "ios.lifecycle.protected_data_unavailable", + "ios.lifecycle.operation_failed", "ios.lifecycle.operation_completed", + "ios.lifecycle.shutdown_requested", "ios.diagnostics.redacted", + ] + static let phases: Set<String> = [ + "starting", "identity_required", "identity_locked", "protected_data_unavailable", + "recovery_required", "corrupt_identity", "configuration_reconfiguration_required", + "running", "stopped", "failed", + ] + private static let operations: Set<String> = [ + "start", "stop", "identity_create", "identity_import", "identity_lock", "identity_unlock", + "identity_recover", "configuration_reconfigure", "settings_reconfigure", "identity_key_removal", + ] + + static func sanitized(_ event: RadrootsTelemetryEvent) -> RadrootsTelemetryEvent? { + let known = eventNames.contains(event.name) + let name = known ? event.name : "ios.diagnostics.redacted" + let fields = known ? event.fields.prefix(8).compactMap(sanitizedField) : [] + return try? RadrootsTelemetryEvent( + name: name, category: "ios_lifecycle", level: event.level, + fields: fields, occurredAt: event.occurredAt + ) + } + + private static func sanitizedField(_ field: RadrootsTelemetryField) -> RadrootsTelemetryField? { + if case let .string(value) = field.value { + switch field.key { + case "operation" where operations.contains(value): return field + case "phase" where phases.contains(value): return field + case "code" where value == "bootstrap_failed": return field + default: return nil + } + } + if case let .integer(value) = field.value, + ["count", "attempts", "schema", "latency_ms"].contains(field.key), + (0 ... 1_000_000).contains(value) + { + return field + } + return nil + } + + static func code(_ value: String, allowed: Set<String>) -> String { + allowed.contains(value) ? value : "unavailable" + } + + static func version(_ value: String) -> String { + code(value, allowed: ["0.1.0-alpha"]) + } + + static func build(_ value: String) -> String { + guard !value.isEmpty, value.utf8.count <= 10, + value.utf8.allSatisfy({ (48 ... 57).contains($0) }) + else { return "unavailable" } + return value + } +} + +struct TeraSanitizedTelemetry: RadrootsTelemetry { + let sink: any RadrootsTelemetry + + func record(_ event: RadrootsTelemetryEvent) async { + guard let event = TeraDiagnosticPolicy.sanitized(event) else { return } + await sink.record(event) + } +} diff --git a/Tera/Runtime/TeraLifecycleCoordinator.swift b/Tera/Runtime/TeraLifecycleCoordinator.swift @@ -111,7 +111,6 @@ actor TeraDiagnosticsBuffer: RadrootsTelemetry { } private let capacity: Int - private let policy = RadrootsTelemetryRedactionPolicy.default private var events: [Entry] = [] init(capacity: Int = 128) { @@ -119,6 +118,7 @@ actor TeraDiagnosticsBuffer: RadrootsTelemetry { } func record(_ event: RadrootsTelemetryEvent) { + guard let event = TeraDiagnosticPolicy.sanitized(event) else { return } guard let occurredAtUnixMilliseconds = try? TeraClock.signedUnixMilliseconds( from: event.occurredAt ) else { @@ -126,7 +126,7 @@ actor TeraDiagnosticsBuffer: RadrootsTelemetry { } events.append( Entry( - event: policy.redacted(event), + event: event, occurredAtUnixMilliseconds: occurredAtUnixMilliseconds ) ) @@ -142,11 +142,11 @@ actor TeraDiagnosticsBuffer: RadrootsTelemetry { name: event.name, category: event.category, level: event.level.rawValue, - fields: Dictionary( - uniqueKeysWithValues: event.fields.map { field in - (field.key, field.value.renderedValue) + fields: event.fields.reduce(into: [String: String]()) { values, field in + if values[field.key] == nil { + values[field.key] = field.value.renderedValue } - ), + }, occurredAtUnixMilliseconds: entry.occurredAtUnixMilliseconds ) } @@ -189,7 +189,7 @@ actor TeraLifecycleCoordinator { transfer: (any RadrootsBackgroundTransfer)?, transferIdentifier: String? ) { - self.telemetry = telemetry + self.telemetry = TeraSanitizedTelemetry(sink: telemetry) self.buffer = buffer self.fileAccess = fileAccess self.transfer = transfer @@ -211,7 +211,7 @@ actor TeraLifecycleCoordinator { let logger = RadrootsAppleLoggerTelemetry(subsystem: bundleIdentifier) let telemetry = RadrootsMultiplexTelemetry([ logger, - RadrootsRedactingTelemetry(sink: buffer), + buffer, ]) let identifier = try RadrootsBackgroundTransferValidation.normalizedIdentifier( TeraRemoteQualificationEnvironment.backgroundTransferIdentifier( @@ -238,7 +238,7 @@ actor TeraLifecycleCoordinator { static func disabled() -> TeraLifecycleCoordinator { let buffer = TeraDiagnosticsBuffer() return TeraLifecycleCoordinator( - telemetry: RadrootsRedactingTelemetry(sink: buffer), + telemetry: buffer, buffer: buffer, fileAccess: nil, transfer: nil, @@ -251,7 +251,7 @@ actor TeraLifecycleCoordinator { { let buffer = TeraDiagnosticsBuffer(capacity: capacity) return TeraLifecycleCoordinator( - telemetry: RadrootsRedactingTelemetry(sink: buffer), + telemetry: buffer, buffer: buffer, fileAccess: RadrootsAppleFileAccess(roots: roots), transfer: nil, @@ -296,7 +296,7 @@ actor TeraLifecycleCoordinator { level: RadrootsTelemetryLevel = .info, fields: [String: String] = [:] ) async { - let values = fields.sorted(by: { $0.key < $1.key }).compactMap { key, value in + let values = fields.prefix(8).sorted(by: { $0.key < $1.key }).compactMap { key, value in try? RadrootsTelemetryField.string(key, value) } guard @@ -330,13 +330,13 @@ actor TeraLifecycleCoordinator { let records = await buffer.records() let document = TeraDiagnosticsDocument( schema: "radroots.ios.diagnostics.v1", - appVersion: appVersion, - appBuild: appBuild, - runtimeCrate: snapshot.crateName, - runtimeVersion: snapshot.crateVersion, - runtimePhase: phase, - relayProfile: snapshot.relay?.profile, - relayState: snapshot.relay?.state, + appVersion: TeraDiagnosticPolicy.version(appVersion), + appBuild: TeraDiagnosticPolicy.build(appBuild), + runtimeCrate: TeraDiagnosticPolicy.code(snapshot.crateName, allowed: ["tera_ffi"]), + runtimeVersion: TeraDiagnosticPolicy.version(snapshot.crateVersion), + runtimePhase: TeraDiagnosticPolicy.code(phase, allowed: TeraDiagnosticPolicy.phases), + relayProfile: snapshot.relay.map { TeraDiagnosticPolicy.code($0.profile, allowed: ["public", "simulator_local", "simulator", "device_development", "unknown"]) }, + relayState: snapshot.relay.map { TeraDiagnosticPolicy.code($0.state, allowed: ["configured", "connecting", "read_only", "writable", "degraded", "offline", "failed", "closed", "unknown"]) }, relayCount: snapshot.relay?.relays.count ?? 0, records: records ) diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 79562256, + "bytes": 79610952, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "5c1a6aa24e17b0a663a4cf18cdac0fff5c8579dc9dc8218625f2e65088b6db15" + "sha256": "648555098c7aa327a3f3e2e113a5868aef6d438c985a1f2c73e7f015349944a7" }, { "bytes": 105627, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 79590016, + "bytes": 79646800, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "ccbdc32bd64fd669f874c87b51b8faea76cd5a8fca7a981eabdc63bad0de3536" + "sha256": "d7b5bce06f98b202c676e2022377d006459be8572aa1bccf4d902e5247b69238" }, { "bytes": 64990, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 23629376, + "bytes": 23644912, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "91fa528d7e5cf05594ab07971135c416390e3db4060f3eba42571b01976c09ee" + "sha256": "30d66d614f0aadfd6ab870f4281f2986a2dfd93f4046bb6a12257d83153c7a9c" }, { - "bytes": 79562256, + "bytes": 79610952, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "5c1a6aa24e17b0a663a4cf18cdac0fff5c8579dc9dc8218625f2e65088b6db15" + "sha256": "648555098c7aa327a3f3e2e113a5868aef6d438c985a1f2c73e7f015349944a7" }, { - "bytes": 79590016, + "bytes": 79646800, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "ccbdc32bd64fd669f874c87b51b8faea76cd5a8fca7a981eabdc63bad0de3536" + "sha256": "d7b5bce06f98b202c676e2022377d006459be8572aa1bccf4d902e5247b69238" }, { - "bytes": 137498, + "bytes": 138031, "path": "source/aarch64-apple-darwin.json", - "sha256": "a8c261d06aa74c41acdbbc9fc04ece8b82d2eb1a913c8a0d660be90dcc4a1b59" + "sha256": "c227dc0072bf5f333f0da087fd72906cb657d8b3be4442c032292b6ebc625321" }, { - "bytes": 137342, + "bytes": 137875, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "a50bfff47625d38b61181db481bd1162e985f1a0506a1f04c5a8e4be9fe99d63" + "sha256": "267f13ec71cfccda3d2d9bc6600fab446fde9fda4b7e0afd31011aeabe96785e" }, { - "bytes": 137338, + "bytes": 137871, "path": "source/aarch64-apple-ios.json", - "sha256": "2c620e09300f6b23da57978a04d153f18dc7b4f101081810b2eff467db636392" + "sha256": "c1dc9e9a167368169dfd74cef909497c204a41c7da76d00a12f5ba4fbbcf7c85" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "82b8d14996d9b088101ca9f21c82c1200d737d96" + "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 79562256, + "bytes": 79610952, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "5c1a6aa24e17b0a663a4cf18cdac0fff5c8579dc9dc8218625f2e65088b6db15" + "sha256": "648555098c7aa327a3f3e2e113a5868aef6d438c985a1f2c73e7f015349944a7" }, { "bytes": 105627, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 79590016, + "bytes": 79646800, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "ccbdc32bd64fd669f874c87b51b8faea76cd5a8fca7a981eabdc63bad0de3536" + "sha256": "d7b5bce06f98b202c676e2022377d006459be8572aa1bccf4d902e5247b69238" }, { "bytes": 807948, @@ -174,19 +174,19 @@ "sha256": "5d74060f97e52dad69ab7567c16d6b6b34acfab846f468450d89e2394a8a69d5" }, { - "bytes": 137498, + "bytes": 138031, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "a8c261d06aa74c41acdbbc9fc04ece8b82d2eb1a913c8a0d660be90dcc4a1b59" + "sha256": "c227dc0072bf5f333f0da087fd72906cb657d8b3be4442c032292b6ebc625321" }, { - "bytes": 137342, + "bytes": 137875, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "a50bfff47625d38b61181db481bd1162e985f1a0506a1f04c5a8e4be9fe99d63" + "sha256": "267f13ec71cfccda3d2d9bc6600fab446fde9fda4b7e0afd31011aeabe96785e" }, { - "bytes": 137338, + "bytes": 137871, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "2c620e09300f6b23da57978a04d153f18dc7b4f101081810b2eff467db636392" + "sha256": "c1dc9e9a167368169dfd74cef909497c204a41c7da76d00a12f5ba4fbbcf7c85" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "82b8d14996d9b088101ca9f21c82c1200d737d96" -manifest_sha256 = "f85af700b201d5fbe036a489bb42e8ab88c63b8efb4f6c78e279131e1190b27d" +source_tree = "8f9abc83606e9996524f519b8bfb18dc3348590a" +manifest_sha256 = "feaf4681644f041ce4bf841cf1e3c29b03bba15c9de9858f1c0a1ccf63419993" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -2092,10 +2092,16 @@ "sha256": "d9eccaf768ee3907498b399c317ad5a2e26d73e345cf55a952eabe4f5c508775" }, "core/crates/tera_ffi/src/logging.rs": { - "bytes": 9313, - "git_blob": "aaed371f0f074bfa2d2ccb6d5e958c75d066fff6", + "bytes": 9488, + "git_blob": "c54092b280d199f3d8178d9d986569eb88a7d107", "mode": "100644", - "sha256": "59911489707924bb42302894800a77551f9038b56f783e4c634bd66d800f92a2" + "sha256": "39a0bef89336f974062bacba8140db2dd1c14c3353a3ae256caddeb293523c85" + }, + "core/crates/tera_ffi/src/logging/privacy_format.rs": { + "bytes": 2342, + "git_blob": "b6e2c643a541c42de081a35e71f3772c8699b543", + "mode": "100644", + "sha256": "d699ff796db7e0c49f736a0a24915702d25725c9f19f4551b6771f6de8f8cced" }, "core/crates/tera_ffi/src/logging/writer.rs": { "bytes": 15077, @@ -2337,6 +2343,12 @@ "mode": "100644", "sha256": "ec87c2cc7f396ef583d3a37186f40932ac4eb42c0e80973a5c25312099f2670a" }, + "core/crates/tera_ffi/tests/logging_privacy.rs": { + "bytes": 3917, + "git_blob": "540fb276cddf7d2760c1ef6b7f2d6c332fd8968c", + "mode": "100644", + "sha256": "0a8ef753b0ebec0be7bdff235b2c0b3da3461a1b1240ca3765d6da5c8597fe27" + }, "core/crates/tera_ffi/tests/media_ownership.rs": { "bytes": 6567, "git_blob": "57a09e25c1db661ecacb26afe95491f462cb52f8", @@ -2920,13 +2932,13 @@ "sha256": "4e174e14512c1b12994dea63f58bf39d234d96c3bf245eb5d7eaa0fcf2b4fd24" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 186460, - "git_blob": "ca81e3e1c1628650dceb01ebd2d3e6046c7a9b00", + "bytes": 186929, + "git_blob": "39860837f76433ef305fde969d9162fefa98e579", "mode": "100644", - "sha256": "7f5053f449d49ff9220e2ba57db5749c813dfe40f04965b0107ce7e49a15966c" + "sha256": "0e7b8f85e58a1b34c27ebbaa03b21f30e54efd9f4998462dda8b2bba93cb54df" } }, "policy": "staged_inputs", - "tree": "82b8d14996d9b088101ca9f21c82c1200d737d96" + "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -2088,10 +2088,16 @@ "sha256": "d9eccaf768ee3907498b399c317ad5a2e26d73e345cf55a952eabe4f5c508775" }, "core/crates/tera_ffi/src/logging.rs": { - "bytes": 9313, - "git_blob": "aaed371f0f074bfa2d2ccb6d5e958c75d066fff6", + "bytes": 9488, + "git_blob": "c54092b280d199f3d8178d9d986569eb88a7d107", "mode": "100644", - "sha256": "59911489707924bb42302894800a77551f9038b56f783e4c634bd66d800f92a2" + "sha256": "39a0bef89336f974062bacba8140db2dd1c14c3353a3ae256caddeb293523c85" + }, + "core/crates/tera_ffi/src/logging/privacy_format.rs": { + "bytes": 2342, + "git_blob": "b6e2c643a541c42de081a35e71f3772c8699b543", + "mode": "100644", + "sha256": "d699ff796db7e0c49f736a0a24915702d25725c9f19f4551b6771f6de8f8cced" }, "core/crates/tera_ffi/src/logging/writer.rs": { "bytes": 15077, @@ -2333,6 +2339,12 @@ "mode": "100644", "sha256": "ec87c2cc7f396ef583d3a37186f40932ac4eb42c0e80973a5c25312099f2670a" }, + "core/crates/tera_ffi/tests/logging_privacy.rs": { + "bytes": 3917, + "git_blob": "540fb276cddf7d2760c1ef6b7f2d6c332fd8968c", + "mode": "100644", + "sha256": "0a8ef753b0ebec0be7bdff235b2c0b3da3461a1b1240ca3765d6da5c8597fe27" + }, "core/crates/tera_ffi/tests/media_ownership.rs": { "bytes": 6567, "git_blob": "57a09e25c1db661ecacb26afe95491f462cb52f8", @@ -2916,13 +2928,13 @@ "sha256": "4e174e14512c1b12994dea63f58bf39d234d96c3bf245eb5d7eaa0fcf2b4fd24" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 186460, - "git_blob": "ca81e3e1c1628650dceb01ebd2d3e6046c7a9b00", + "bytes": 186929, + "git_blob": "39860837f76433ef305fde969d9162fefa98e579", "mode": "100644", - "sha256": "7f5053f449d49ff9220e2ba57db5749c813dfe40f04965b0107ce7e49a15966c" + "sha256": "0e7b8f85e58a1b34c27ebbaa03b21f30e54efd9f4998462dda8b2bba93cb54df" } }, "policy": "staged_inputs", - "tree": "82b8d14996d9b088101ca9f21c82c1200d737d96" + "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -2088,10 +2088,16 @@ "sha256": "d9eccaf768ee3907498b399c317ad5a2e26d73e345cf55a952eabe4f5c508775" }, "core/crates/tera_ffi/src/logging.rs": { - "bytes": 9313, - "git_blob": "aaed371f0f074bfa2d2ccb6d5e958c75d066fff6", + "bytes": 9488, + "git_blob": "c54092b280d199f3d8178d9d986569eb88a7d107", "mode": "100644", - "sha256": "59911489707924bb42302894800a77551f9038b56f783e4c634bd66d800f92a2" + "sha256": "39a0bef89336f974062bacba8140db2dd1c14c3353a3ae256caddeb293523c85" + }, + "core/crates/tera_ffi/src/logging/privacy_format.rs": { + "bytes": 2342, + "git_blob": "b6e2c643a541c42de081a35e71f3772c8699b543", + "mode": "100644", + "sha256": "d699ff796db7e0c49f736a0a24915702d25725c9f19f4551b6771f6de8f8cced" }, "core/crates/tera_ffi/src/logging/writer.rs": { "bytes": 15077, @@ -2333,6 +2339,12 @@ "mode": "100644", "sha256": "ec87c2cc7f396ef583d3a37186f40932ac4eb42c0e80973a5c25312099f2670a" }, + "core/crates/tera_ffi/tests/logging_privacy.rs": { + "bytes": 3917, + "git_blob": "540fb276cddf7d2760c1ef6b7f2d6c332fd8968c", + "mode": "100644", + "sha256": "0a8ef753b0ebec0be7bdff235b2c0b3da3461a1b1240ca3765d6da5c8597fe27" + }, "core/crates/tera_ffi/tests/media_ownership.rs": { "bytes": 6567, "git_blob": "57a09e25c1db661ecacb26afe95491f462cb52f8", @@ -2916,13 +2928,13 @@ "sha256": "4e174e14512c1b12994dea63f58bf39d234d96c3bf245eb5d7eaa0fcf2b4fd24" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 186460, - "git_blob": "ca81e3e1c1628650dceb01ebd2d3e6046c7a9b00", + "bytes": 186929, + "git_blob": "39860837f76433ef305fde969d9162fefa98e579", "mode": "100644", - "sha256": "7f5053f449d49ff9220e2ba57db5749c813dfe40f04965b0107ce7e49a15966c" + "sha256": "0e7b8f85e58a1b34c27ebbaa03b21f30e54efd9f4998462dda8b2bba93cb54df" } }, "policy": "staged_inputs", - "tree": "82b8d14996d9b088101ca9f21c82c1200d737d96" + "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a" } } diff --git a/TeraTests/TeraDiagnosticPolicyTests.swift b/TeraTests/TeraDiagnosticPolicyTests.swift @@ -0,0 +1,69 @@ +import Foundation +import RadrootsKit +@testable import TeraApp +import XCTest + +final class TeraDiagnosticPolicyTests: XCTestCase { + func testAllSinksReceiveOnlyClosedFieldsAndNoRawMessageCategoryOrValues() async throws { + let capture = DiagnosticSink() + let buffer = TeraDiagnosticsBuffer() + let telemetry = TeraSanitizedTelemetry(sink: RadrootsMultiplexTelemetry([capture, buffer])) + let canary = "ordinary private text 49.123456,-123.987654 Bearer CANARY_TOKEN /Users/private/file" + let event = try RadrootsTelemetryEvent( + name: "ios.lifecycle.operation_completed", category: "private_category", message: canary, + fields: [ + .string("operation", "start"), .string("phase", "running"), + .string("code", canary), .string("authorization", canary), + .string("plain", canary), .integer("count", 7), .integer("id", 123_456_789), + .stringList("list", [canary]), .double("latitude", 49.123456), + ] + ) + await telemetry.record(event) + let observed = await capture.events + let safe = try XCTUnwrap(observed.first) + XCTAssertNil(safe.message) + XCTAssertEqual(safe.category, "ios_lifecycle") + XCTAssertEqual(safe.fields.map(\.key), ["operation", "phase", "count"]) + XCTAssertFalse(String(describing: safe).contains(canary)) + let records = await buffer.records() + XCTAssertEqual(records.first?.fields, ["operation": "start", "phase": "running", "count": "7"]) + XCTAssertEqual(TeraDiagnosticPolicy.sanitized(safe), safe) + } + + func testDirectBufferRejectsUnknownNamesKeysAndHighCardinalityValues() async throws { + let buffer = TeraDiagnosticsBuffer(capacity: 16) + for index in 0 ..< 40 { + let event = try RadrootsTelemetryEvent( + name: "private.post.\(index)", category: "private_location", + message: "Raw private content", fields: [.string("unknown", "Bearer canary")] + ) + await buffer.record(event) + } + let records = await buffer.records() + XCTAssertEqual(records.count, 16) + XCTAssertTrue(records.allSatisfy { $0.name == "ios.diagnostics.redacted" && $0.category == "ios_lifecycle" && $0.fields.isEmpty }) + let event = try RadrootsTelemetryEvent(name: "ios.lifecycle.active", fields: [ + .integer("count", Int64.max), .integer("schema", -1), .string("phase", "secret"), + ]) + await buffer.record(event) + let final = await buffer.records() + XCTAssertEqual(final.last?.fields, [:]) + } + + func testMetadataUsesFiniteCodesAndBoundedNumericBuilds() { + let canary = "public key or private content /Users/person/file" + XCTAssertEqual(TeraDiagnosticPolicy.version(canary), "unavailable") + XCTAssertEqual(TeraDiagnosticPolicy.version("0.1.0-alpha"), "0.1.0-alpha") + XCTAssertEqual(TeraDiagnosticPolicy.build(canary), "unavailable") + XCTAssertEqual(TeraDiagnosticPolicy.build(String(repeating: "1", count: 11)), "unavailable") + XCTAssertEqual(TeraDiagnosticPolicy.build("1"), "1") + XCTAssertEqual(TeraDiagnosticPolicy.code(canary, allowed: TeraDiagnosticPolicy.phases), "unavailable") + } +} + +private actor DiagnosticSink: RadrootsTelemetry { + var events: [RadrootsTelemetryEvent] = [] + func record(_ event: RadrootsTelemetryEvent) { + events.append(event) + } +} diff --git a/TeraTests/TeraLifecycleTests.swift b/TeraTests/TeraLifecycleTests.swift @@ -115,7 +115,8 @@ final class TeraLifecycleTests: XCTestCase { XCTAssertEqual(object["schema"] as? String, "radroots.ios.diagnostics.v1") XCTAssertEqual(records.count, 16) XCTAssertLessThanOrEqual(data.count, 256 * 1024) - XCTAssertTrue(text.contains("[redacted]")) + XCTAssertTrue(text.contains("ios.diagnostics.redacted")) + XCTAssertTrue(records.allSatisfy { ($0["fields"] as? [String: String])?.isEmpty == true }) XCTAssertFalse(text.contains(secret)) XCTAssertFalse(text.contains(privateKey)) XCTAssertFalse(text.contains(privatePath)) @@ -125,6 +126,28 @@ final class TeraLifecycleTests: XCTestCase { XCTAssertFalse(FileManager.default.fileExists(atPath: export.fileURL.path)) } + func testActualExportExcludesUntrustedMetadataAndOrdinaryPrivateContent() async throws { + let roots = try makeRoots() + defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) } + let coordinator = TeraLifecycleCoordinator.testing(roots: roots) + let canary = "plain post at 49.123456,-123.987654 Bearer CANARY /Users/private/file" + await coordinator.record("private_event", fields: ["value": canary, "phase": canary]) + let export = try await coordinator.prepareDiagnostics( + snapshot: makeSnapshot(privateKey: String(repeating: "ab", count: 32), metadata: canary), + appVersion: canary, appBuild: canary, phase: canary + ) + let data = try Data(contentsOf: export.fileURL) + let text = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertFalse(text.contains(canary)) + let document = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) + for key in ["appVersion", "appBuild", "runtimeCrate", "runtimeVersion", "runtimePhase", "relayProfile", "relayState"] { + XCTAssertEqual(document[key] as? String, "unavailable", key) + } + XCTAssertLessThanOrEqual(data.count, 256 * 1024) + await coordinator.releaseDiagnostics(export) + XCTAssertFalse(FileManager.default.fileExists(atPath: export.fileURL.path)) + } + private func makeRoots() throws -> RadrootsAppleFileRoots { let base = FileManager.default.temporaryDirectory .appendingPathComponent( @@ -138,15 +161,15 @@ final class TeraLifecycleTests: XCTestCase { ) } - private func makeSnapshot(privateKey: String) -> TeraRuntimeSnapshot { + private func makeSnapshot(privateKey: String, metadata: String? = nil) -> TeraRuntimeSnapshot { TeraRuntimeSnapshot( identity: TeraRuntimeIdentity( publicKeyHex: privateKey, hostSignerConfigured: true ), relay: TeraRelayStatus( - profile: "simulator", - state: "configured", + profile: metadata ?? "simulator", + state: metadata ?? "configured", readAvailability: "available", writeAvailability: "available", relays: [ @@ -164,8 +187,8 @@ final class TeraLifecycleTests: XCTestCase { ), blossomConfiguration: nil, blossomEvidence: nil, - crateName: "tera_ffi", - crateVersion: "0.1.0-alpha", + crateName: metadata ?? "tera_ffi", + crateVersion: metadata ?? "0.1.0-alpha", isClosed: false ) } diff --git a/core/crates/tera_ffi/src/logging.rs b/core/crates/tera_ffi/src/logging.rs @@ -1,3 +1,4 @@ +mod privacy_format; mod writer; use std::fs; @@ -76,12 +77,12 @@ fn initialize(options: LoggingOptions) -> Result<(), String> { tracing_subscriber::fmt::layer() .with_writer(writer.clone()) .with_ansi(false) - .with_target(false) + .event_format(privacy_format::DiagnosticFormat) }); let stdout_layer = options.stdout.then(|| { tracing_subscriber::fmt::layer() .with_writer(std::io::stdout) - .with_target(false) + .event_format(privacy_format::DiagnosticFormat) }); tracing_subscriber::registry() .with(file_layer) @@ -148,19 +149,22 @@ pub fn init_logging_stdout() -> Result<(), crate::TeraAppError> { #[cfg_attr(not(coverage_nightly), uniffi::export)] pub fn log_info(msg: String) -> Result<(), crate::TeraAppError> { - tracing::info!("{msg}"); + drop(msg); + tracing::info!(code = "message_redacted"); Ok(()) } #[cfg_attr(not(coverage_nightly), uniffi::export)] pub fn log_error(msg: String) -> Result<(), crate::TeraAppError> { - tracing::error!("{msg}"); + drop(msg); + tracing::error!(code = "message_redacted"); Ok(()) } #[cfg_attr(not(coverage_nightly), uniffi::export)] pub fn log_debug(msg: String) -> Result<(), crate::TeraAppError> { - tracing::debug!("{msg}"); + drop(msg); + tracing::debug!(code = "message_redacted"); Ok(()) } diff --git a/core/crates/tera_ffi/src/logging/privacy_format.rs b/core/crates/tera_ffi/src/logging/privacy_format.rs @@ -0,0 +1,81 @@ +//! Closed diagnostics at both native logging sinks. Raw event messages, +//! span fields, targets, identifiers and paths are never formatted. + +use std::fmt; +use tracing::{ + Event, Subscriber, + field::{Field, Visit}, +}; +use tracing_subscriber::{ + fmt::{FmtContext, FormatEvent, FormatFields, format::Writer}, + registry::LookupSpan, +}; + +pub(super) struct DiagnosticFormat; + +#[derive(Default)] +struct DiagnosticFields { + code: Option<&'static str>, + counts: Vec<(&'static str, u64)>, +} + +impl Visit for DiagnosticFields { + fn record_debug(&mut self, _field: &Field, _value: &dyn fmt::Debug) {} + + fn record_str(&mut self, field: &Field, value: &str) { + if field.name() == "code" { + self.code = match value { + "message_redacted" => Some("message_redacted"), + "runtime_started" => Some("runtime_started"), + "runtime_stopped" => Some("runtime_stopped"), + "operation_failed" => Some("operation_failed"), + _ => None, + }; + } + } + + fn record_u64(&mut self, field: &Field, value: u64) { + let key = match field.name() { + "count" => "count", + "attempts" => "attempts", + "schema" => "schema", + "latency_ms" => "latency_ms", + _ => return, + }; + if value <= 1_000_000 && self.counts.len() < 4 { + self.counts.push((key, value)); + } + } + + fn record_i64(&mut self, field: &Field, value: i64) { + if let Ok(value) = u64::try_from(value) { + self.record_u64(field, value); + } + } +} + +impl<S, N> FormatEvent<S, N> for DiagnosticFormat +where + S: Subscriber + for<'lookup> LookupSpan<'lookup>, + N: for<'writer> FormatFields<'writer> + 'static, +{ + fn format_event( + &self, + _context: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + let mut fields = DiagnosticFields::default(); + event.record(&mut fields); + write!( + writer, + "level={} code={}", + event.metadata().level(), + fields.code.unwrap_or("message_redacted") + )?; + for (key, value) in fields.counts { + write!(writer, " {key}={value}")?; + } + writeln!(writer) + } +} diff --git a/core/crates/tera_ffi/tests/logging_privacy.rs b/core/crates/tera_ffi/tests/logging_privacy.rs @@ -0,0 +1,107 @@ +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; + +const CHILD_ROOT: &str = "TERA_LOG_PRIVACY_CHILD_ROOT"; +const TEST_NAME: &str = "actual_file_and_stdout_sinks_exclude_private_material"; +const CANARIES: [&str; 7] = [ + "plain private post text without a redaction keyword", + "Bearer CANARY_AUTHORIZATION_VALUE", + "49.123456,-123.987654 precise location", + "/Users/private/canary.json", + "nsec1CANARY_SECRET", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "PRIVATE_TARGET_CANARY", +]; + +#[test] +fn actual_file_and_stdout_sinks_exclude_private_material() { + if let Some(root) = std::env::var_os(CHILD_ROOT) { + emit_and_drain(std::path::Path::new(&root)); + return; + } + let root = tempfile::tempdir().unwrap(); + let mut child = Command::new(std::env::current_exe().unwrap()) + .args(["--exact", TEST_NAME, "--nocapture"]) + .env(CHILD_ROOT, root.path()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(10); + loop { + if child.try_wait().unwrap().is_some() { + break; + } + if Instant::now() >= deadline { + child.kill().unwrap(); + child.wait().unwrap(); + panic!("bounded diagnostics sink process did not finish"); + } + std::thread::sleep(Duration::from_millis(10)); + } + let output = child.wait_with_output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + let file = std::fs::read_to_string(root.path().join("privacy.log")).unwrap(); + assert_safe(&stdout); + assert_safe(&file); + for canary in CANARIES { + assert!(!stderr.contains(canary)); + } + let lines = stdout + .lines() + .filter(|line| line.starts_with("level=")) + .collect::<Vec<_>>(); + assert_eq!(lines.len(), 6); + assert_eq!(lines, file.lines().collect::<Vec<_>>()); +} + +fn emit_and_drain(root: &std::path::Path) { + tera_ffi::logging::init_logging( + Some(root.to_str().unwrap().to_owned()), + Some("privacy.log".to_owned()), + Some(true), + ) + .unwrap(); + tera_ffi::logging::log_info(CANARIES[0].to_owned()).unwrap(); + tera_ffi::logging::log_error(CANARIES[1].to_owned()).unwrap(); + tera_ffi::logging::log_debug(CANARIES[2].to_owned()).unwrap(); + let span = tracing::info_span!("private_span", path = CANARIES[3], secret = CANARIES[4]); + let _entered = span.enter(); + tracing::error!(target: "PRIVATE_TARGET_CANARY", authorization=CANARIES[1], content=CANARIES[0], location=CANARIES[2], id=CANARIES[5], message=CANARIES[3]); + tracing::info!( + code = "runtime_started", + count = 7_u64, + attempts = 2_i64, + schema = 1_u64, + latency_ms = 9_u64 + ); + tracing::warn!(code = CANARIES[0], count = u64::MAX, unknown = CANARIES[4]); + let path = root.join("privacy.log"); + let deadline = Instant::now() + Duration::from_secs(5); + loop { + let text = std::fs::read_to_string(&path).unwrap_or_default(); + if text.lines().count() >= 6 { + assert_safe(&text); + return; + } + assert!( + Instant::now() < deadline, + "bounded owned writer did not drain" + ); + std::thread::sleep(Duration::from_millis(10)); + } +} + +fn assert_safe(text: &str) { + for canary in CANARIES { + assert!(!text.contains(canary)); + } + assert!(!text.contains("private_span")); + assert!(!text.contains(&u64::MAX.to_string())); + assert!(text.contains("code=runtime_started count=7 attempts=2 schema=1 latency_ms=9")); + assert!(text.contains("level=ERROR code=message_redacted")); + assert!(text.len() < 4096); +} diff --git a/project.yml b/project.yml @@ -163,6 +163,7 @@ targets: - path: TeraTests/TeraCapacityRecoveryTests.swift - path: TeraTests/TeraCapacitySettingsTests.swift - path: TeraTests/TeraLifecycleTests.swift + - path: TeraTests/TeraDiagnosticPolicyTests.swift - path: TeraTests/TeraClockTests.swift - path: TeraTests/TeraCalendarFFICompatibilityTests.swift - path: TeraTests/TeraCalendarTimingTests.swift diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "5d74060f97e52dad69ab7567c16d6b6b34acfab846f468450d89e2394a8a69d5", - "ffi_provenance_sha256": "f85af700b201d5fbe036a489bb42e8ab88c63b8efb4f6c78e279131e1190b27d", + "ffi_provenance_sha256": "feaf4681644f041ce4bf841cf1e3c29b03bba15c9de9858f1c0a1ccf63419993", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "9e243176e3eaec256f404207d284314e8c96d3c6861077acb8e760640047ee2f", - "xcode_project_sha256": "e5346577077bbb48a52caf97c7088d2cbebddca5a76b70855afd60c7aa99cc68" + "xcode_project_sha256": "6ad5a40e50a198c65afa5e5f78d229c951b618d8ee98e56e49aa5ee7596ff763" }, "disposition": "unsigned", "platforms": [ @@ -22,7 +22,7 @@ "lib_revision": "189c49b74b4bafc142b00b76b296477931139e72", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "322eaec80d4b85ef9eb20da95ff95c4d26d8555ca358ba0c8eb890adf0b6671e", - "tera_ffi_source_tree": "82b8d14996d9b088101ca9f21c82c1200d737d96", + "tera_ffi_source_tree": "8f9abc83606e9996524f519b8bfb18dc3348590a", "xcode_package_lock_sha256": "4e29fed46339d5e382fa78ec85b61be4296455ece4ddc40602d793c049e673b3" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -1765,6 +1765,10 @@ "count": 1 }, { + "path": "Tera/Runtime/TeraDiagnosticPolicy.swift", + "count": 1 + }, + { "path": "Tera/Runtime/TeraDiagnosticsStore.swift", "count": 1 }, @@ -1881,6 +1885,10 @@ "count": 1 }, { + "path": "TeraTests/TeraDiagnosticPolicyTests.swift", + "count": 1 + }, + { "path": "TeraTests/TeraDurableMediaRootsTests.swift", "count": 1 }, @@ -2195,6 +2203,10 @@ { "path": "Tera/Runtime/TeraLifecycleCoordinator.swift", "count": 1 + }, + { + "path": "TeraTests/TeraDiagnosticPolicyTests.swift", + "count": 1 } ] }, @@ -2257,16 +2269,6 @@ ] }, { - "identifier": "RadrootsRedactingTelemetry", - "category": "shared_apple_api", - "occurrences": [ - { - "path": "Tera/Runtime/TeraLifecycleCoordinator.swift", - "count": 3 - } - ] - }, - { "identifier": "RadrootsRhiEvidenceAttestationOutcomeV1", "category": "shared_foundation_api", "occurrences": [ @@ -2425,8 +2427,16 @@ "category": "shared_apple_api", "occurrences": [ { + "path": "Tera/Runtime/TeraDiagnosticPolicy.swift", + "count": 2 + }, + { "path": "Tera/Runtime/TeraLifecycleCoordinator.swift", "count": 3 + }, + { + "path": "TeraTests/TeraDiagnosticPolicyTests.swift", + "count": 1 } ] }, @@ -2455,27 +2465,35 @@ "category": "shared_apple_api", "occurrences": [ { + "path": "Tera/Runtime/TeraDiagnosticPolicy.swift", + "count": 4 + }, + { "path": "Tera/Runtime/TeraLifecycleCoordinator.swift", "count": 3 }, { + "path": "TeraTests/TeraDiagnosticPolicyTests.swift", + "count": 5 + }, + { "path": "TeraTests/TeraLifecycleTests.swift", "count": 1 } ] }, { - "identifier": "RadrootsTelemetryField.string", + "identifier": "RadrootsTelemetryField", "category": "shared_apple_api", "occurrences": [ { - "path": "Tera/Runtime/TeraLifecycleCoordinator.swift", - "count": 1 + "path": "Tera/Runtime/TeraDiagnosticPolicy.swift", + "count": 2 } ] }, { - "identifier": "RadrootsTelemetryLevel", + "identifier": "RadrootsTelemetryField.string", "category": "shared_apple_api", "occurrences": [ { @@ -2485,7 +2503,7 @@ ] }, { - "identifier": "RadrootsTelemetryRedactionPolicy.default", + "identifier": "RadrootsTelemetryLevel", "category": "shared_apple_api", "occurrences": [ {