Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Sources/Support/ClipboardRestoringTextPaster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,10 @@ private struct FocusedTextPasteConfirmation {
return nil
}

// Key names here must not contain any sensitive-key fragment from
// PayloadSanitizationCore (e.g. "text", "name") or the local sanitizer
// blanks the boolean to "[redacted-sensitive-value]" in events.jsonl.
// "target_value_observable" reports whether kAXValueAttribute was readable.
func diagnosticsContext(
clipboardReadAt: CFAbsoluteTime?,
pasteDispatchedAt: CFAbsoluteTime
Expand All @@ -543,7 +547,7 @@ private struct FocusedTextPasteConfirmation {
"target_change_after_dispatch": "\((changeObserver?.changedAt ?? 0) >= pasteDispatchedAt)",
"target_change_observer_available": "\(changeObserver != nil)",
"target_selection_observable": "\(initialSelectionRange != nil)",
"target_text_observable": "\(initialValue != nil)",
"target_value_observable": "\(initialValue != nil)",
]
}

Expand Down Expand Up @@ -814,7 +818,7 @@ final class ClipboardRestoringTextPaster {
"target_change_after_dispatch": "false",
"target_change_observer_available": "false",
"target_selection_observable": "false",
"target_text_observable": "false",
"target_value_observable": "false",
]
let targetStillFrontmost = pasteConfirmationResult == .unconfirmed
diagnostics["target_still_frontmost"] = "\(targetStillFrontmost)"
Expand Down
71 changes: 70 additions & 1 deletion Tests/ClipboardRestoringTextPasterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,75 @@ func testClipboardRestoringTextPaster() async {
}
}

runSuite("ClipboardRestoringTextPaster confirmation diagnostics survive the local event sanitizer") {
// Regression: "target_text_observable" contained the sensitive fragment
// "text", so LocalObservabilityPayloadSanitizer blanked the boolean to
// "[redacted-sensitive-value]" in events.jsonl and blinded a paste
// investigation. Pin that every emitted diagnostics key stays readable.
let pasteboard = NSPasteboard(name: NSPasteboard.Name("TranscriptedDiagnosticsSanitizer-\(UUID().uuidString)"))
let paster = ClipboardRestoringTextPaster()
let adapter = SyntheticPasteTargetAdapter(kind: .codex, appliesPaste: false)

pasteboard.clearContents()
pasteboard.setString("synthetic original clipboard", forType: .string)
let outcome = paster.paste(
"synthetic unconfirmed dictation",
pasteboard: pasteboard,
accessibilityTrusted: { true },
requestAccessibilityTrust: {},
pasteDispatcher: { true },
confirmationSource: { adapter },
targetIsFrontmost: { true },
pasteConfirmationWait: 0
)

assertEqual(
outcome.copyReason,
.pasteConfirmationUnavailable,
"an unconfirmed paste with a live confirmation source should report confirmation-unavailable"
)
let diagnostic = paster.lastConfirmationDiagnostic
assertEqual(
diagnostic?.event,
"dictation_paste_confirmation_diagnostics",
"the unconfirmed path should emit the confirmation diagnostics event"
)
let context = diagnostic?.context ?? [:]
assertTrue(
context.keys.contains("target_value_observable"),
"the AX-value observability flag should be present under its sanitizer-safe name"
)
for key in context.keys {
assertFalse(
PayloadSanitizationCore.shouldDrop(
key: key,
sensitiveFragments: LocalObservabilityPayloadSanitizer.sensitiveKeyFragments
),
"diagnostics key \(key) must not match a sensitive fragment or events.jsonl loses the value"
)
}

let sanitized = LocalObservabilityPayloadSanitizer.sanitize(
ObservabilityEvent(
timestamp: "2026-08-24T00:00:00Z",
level: "warning",
engine: "overlay",
event: diagnostic?.event ?? "dictation_paste_confirmation_diagnostics",
message: "Paste delivery could not be confirmed from privacy-safe target signals",
context: context,
appVersion: "1.0.0",
osVersion: "26.0"
)
)
for (key, value) in context {
assertEqual(
sanitized.context?[key],
value,
"diagnostics value for \(key) should reach events.jsonl unredacted"
)
}
}

runSuite("ClipboardRestoringTextPaster.restorePasteboardItems — preserves user clipboard changes") {
let pasteboard = NSPasteboard(name: NSPasteboard.Name("TranscriptedClipboardTest-\(UUID().uuidString)"))
let paster = ClipboardRestoringTextPaster()
Expand Down Expand Up @@ -1762,7 +1831,7 @@ private final class SyntheticPasteTargetAdapter: ClipboardPasteConfirmationSourc
"target_change_after_dispatch": "\((targetChangedAt ?? 0) >= pasteDispatchedAt)",
"target_change_observer_available": kind == .browser ? "true" : "false",
"target_selection_observable": kind == .notes ? "true" : "false",
"target_text_observable": kind == .codex ? "true" : "false",
"target_value_observable": kind == .codex ? "true" : "false",
]
}
}
Expand Down
Loading