@agent-native/core 0.114.6 → 0.114.8
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
- package/corpus/core/src/cli/mcp.ts +124 -0
- package/corpus/core/src/cli/skills-content/help.ts +4 -3
- package/corpus/core/src/cli/skills-content/index.ts +1 -0
- package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
- package/corpus/core/src/cli/skills.ts +115 -13
- package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
- package/corpus/core/src/server/agent-chat-plugin.ts +16 -9
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +19 -7
- package/corpus/templates/analytics/app/hooks/use-chart-tooltip-portal.ts +60 -0
- package/corpus/templates/analytics/app/routes/chart.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-07-20-dashboard-requests-no-longer-stall-without-progress.md +6 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -0
- package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
- package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
- package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
- package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
- package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
- package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
- package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
- package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
- package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
- package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
- package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
- package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
- package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
- package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
- package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
- package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
- package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
- package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
- package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
- package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
- package/corpus/templates/clips/desktop/src/styles.css +956 -134
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
- package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
- package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
- package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
- package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
- package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
- package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/dist/cli/mcp-config-writers.d.ts +2 -2
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +6 -6
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +79 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills-content/help.d.ts +1 -1
- package/dist/cli/skills-content/help.d.ts.map +1 -1
- package/dist/cli/skills-content/help.js +4 -3
- package/dist/cli/skills-content/help.js.map +1 -1
- package/dist/cli/skills-content/index.d.ts +1 -0
- package/dist/cli/skills-content/index.d.ts.map +1 -1
- package/dist/cli/skills-content/index.js +1 -0
- package/dist/cli/skills-content/index.js.map +1 -1
- package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
- package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
- package/dist/cli/skills-content/rewind-skill.js +53 -0
- package/dist/cli/skills-content/rewind-skill.js.map +1 -0
- package/dist/cli/skills.d.ts +6 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +90 -14
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +365 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp/screen-memory-stdio.js +1014 -91
- package/dist/mcp/screen-memory-stdio.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +90 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
- package/dist/mcp-client/screen-memory-local.js +590 -12
- package/dist/mcp-client/screen-memory-local.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-chat-plugin.d.ts +5 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +10 -9
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/mcp-config-writers.ts +6 -4
- package/src/cli/mcp.ts +124 -0
- package/src/cli/skills-content/help.ts +4 -3
- package/src/cli/skills-content/index.ts +1 -0
- package/src/cli/skills-content/rewind-skill.ts +52 -0
- package/src/cli/skills.ts +115 -13
- package/src/mcp/screen-memory-stdio.ts +1360 -104
- package/src/mcp-client/index.ts +9 -0
- package/src/mcp-client/screen-memory-local.ts +847 -13
- package/src/server/agent-chat-plugin.ts +16 -9
|
@@ -77,6 +77,11 @@ libc = "0.2"
|
|
|
77
77
|
core-foundation = "0.10"
|
|
78
78
|
core-graphics = "0.25"
|
|
79
79
|
objc2 = { version = "0.6", features = ["exception"] }
|
|
80
|
+
objc2-app-kit = { version = "0.3", default-features = false, features = [
|
|
81
|
+
"std",
|
|
82
|
+
"NSEvent",
|
|
83
|
+
"block2",
|
|
84
|
+
] }
|
|
80
85
|
# Grapheme-cluster-aware chunking for voice-dictation Unicode typing in
|
|
81
86
|
# terminal apps, so a chunk boundary never splits a multi-scalar emoji/
|
|
82
87
|
# combining-mark sequence across two synthetic keyboard events. Already
|
|
@@ -1,12 +1,78 @@
|
|
|
1
|
-
use std::path::Path;
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
2
|
use std::process::Command;
|
|
3
3
|
|
|
4
4
|
fn main() {
|
|
5
5
|
emit_sentry_env_reruns();
|
|
6
|
+
compile_screen_memory_ocr_helper();
|
|
6
7
|
add_swift_runtime_rpaths();
|
|
7
8
|
tauri_build::build()
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
/// Build the tiny, macOS-only AVFoundation/Vision bridge used by local Screen
|
|
12
|
+
/// Memory OCR. Keeping this outside the Rust dependency graph avoids adding a
|
|
13
|
+
/// large Objective-C binding surface for a single, OS-provided capability.
|
|
14
|
+
fn compile_screen_memory_ocr_helper() {
|
|
15
|
+
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let source = Path::new("native/screen_memory_ocr.swift");
|
|
20
|
+
println!("cargo:rerun-if-changed={}", source.display());
|
|
21
|
+
|
|
22
|
+
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("Cargo sets OUT_DIR"));
|
|
23
|
+
let object = out_dir.join("screen_memory_ocr_helper.o");
|
|
24
|
+
let archive = out_dir.join("libscreen_memory_ocr_helper.a");
|
|
25
|
+
|
|
26
|
+
// `tauri build --target universal-apple-darwin` invokes Cargo once per
|
|
27
|
+
// architecture. swiftc otherwise emits an object for the runner's host
|
|
28
|
+
// architecture, which makes the x86_64 link fail on arm64 CI runners.
|
|
29
|
+
let swift_arch = match std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() {
|
|
30
|
+
Ok("aarch64") => "arm64",
|
|
31
|
+
Ok("x86_64") => "x86_64",
|
|
32
|
+
Ok(arch) => panic!("unsupported macOS OCR helper architecture: {arch}"),
|
|
33
|
+
Err(error) => panic!("Cargo target architecture is required: {error}"),
|
|
34
|
+
};
|
|
35
|
+
let deployment_target =
|
|
36
|
+
std::env::var("MACOSX_DEPLOYMENT_TARGET").unwrap_or_else(|_| "13.0".to_string());
|
|
37
|
+
let swift_target = format!("{swift_arch}-apple-macosx{deployment_target}");
|
|
38
|
+
|
|
39
|
+
let swift_status = Command::new("xcrun")
|
|
40
|
+
.args([
|
|
41
|
+
"swiftc",
|
|
42
|
+
"-parse-as-library",
|
|
43
|
+
"-emit-object",
|
|
44
|
+
"-target",
|
|
45
|
+
&swift_target,
|
|
46
|
+
source.to_str().expect("UTF-8 source path"),
|
|
47
|
+
"-o",
|
|
48
|
+
object.to_str().expect("UTF-8 output path"),
|
|
49
|
+
])
|
|
50
|
+
.status()
|
|
51
|
+
.expect("Xcode's swiftc is required to build the macOS OCR helper");
|
|
52
|
+
assert!(swift_status.success(), "failed to compile macOS OCR helper");
|
|
53
|
+
|
|
54
|
+
let archive_status = Command::new("ar")
|
|
55
|
+
.args([
|
|
56
|
+
"crus",
|
|
57
|
+
archive.to_str().expect("UTF-8 archive path"),
|
|
58
|
+
object.to_str().expect("UTF-8 object path"),
|
|
59
|
+
])
|
|
60
|
+
.status()
|
|
61
|
+
.expect("ar is required to archive the macOS OCR helper");
|
|
62
|
+
assert!(
|
|
63
|
+
archive_status.success(),
|
|
64
|
+
"failed to archive macOS OCR helper"
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
println!("cargo:rustc-link-search=native={}", out_dir.display());
|
|
68
|
+
println!("cargo:rustc-link-lib=static=screen_memory_ocr_helper");
|
|
69
|
+
// The helper is written in Swift, while the rest of the native desktop
|
|
70
|
+
// stack already carries Swift runtime rpaths through ScreenCaptureKit.
|
|
71
|
+
for library in ["swiftCore", "swiftFoundation", "swift_Concurrency"] {
|
|
72
|
+
println!("cargo:rustc-link-lib=dylib={library}");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
10
76
|
fn emit_sentry_env_reruns() {
|
|
11
77
|
for name in [
|
|
12
78
|
"CLIPS_DESKTOP_SENTRY_DSN",
|
|
@@ -47,6 +113,8 @@ fn add_swift_runtime_rpaths() {
|
|
|
47
113
|
// CMTime symbols we touch via raw `msg_send!` / `extern "C"`.
|
|
48
114
|
println!("cargo:rustc-link-lib=framework=AVFoundation");
|
|
49
115
|
println!("cargo:rustc-link-lib=framework=CoreMedia");
|
|
116
|
+
println!("cargo:rustc-link-lib=framework=Vision");
|
|
117
|
+
println!("cargo:rustc-link-lib=framework=CoreGraphics");
|
|
50
118
|
println!("cargo:rustc-link-lib=framework=IOKit");
|
|
51
119
|
|
|
52
120
|
// The screencapturekit crate builds a Swift bridge. Its build script adds
|
|
@@ -54,7 +54,120 @@
|
|
|
54
54
|
//! - AXValue can also be a CFNumber (sliders) or CFURL — we only return
|
|
55
55
|
//! a String when it's a CFString; otherwise "".
|
|
56
56
|
|
|
57
|
-
use serde::Serialize;
|
|
57
|
+
use serde::{Deserialize, Serialize};
|
|
58
|
+
|
|
59
|
+
/// A deliberately small, retention-bound summary of the current accessibility
|
|
60
|
+
/// surface. This is evidence for local Rewind scenes, not a serialised AX tree.
|
|
61
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
62
|
+
#[serde(rename_all = "camelCase")]
|
|
63
|
+
pub struct AccessibilityFingerprint {
|
|
64
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
65
|
+
pub document: Option<AccessibilitySemanticNode>,
|
|
66
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
67
|
+
pub focused: Option<AccessibilitySemanticNode>,
|
|
68
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
69
|
+
pub visible_labels: Vec<String>,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
73
|
+
#[serde(rename_all = "camelCase")]
|
|
74
|
+
pub struct AccessibilitySemanticNode {
|
|
75
|
+
pub role: String,
|
|
76
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
77
|
+
pub title: Option<String>,
|
|
78
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
79
|
+
pub description: Option<String>,
|
|
80
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
81
|
+
pub document: Option<String>,
|
|
82
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
83
|
+
pub url: Option<String>,
|
|
84
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
85
|
+
pub selected_text: Option<String>,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const MAX_AX_NODES: usize = 6;
|
|
89
|
+
const MAX_AX_DEPTH: usize = 1;
|
|
90
|
+
const MAX_AX_STRING_BYTES: usize = 160;
|
|
91
|
+
const MAX_AX_LABELS: usize = 8;
|
|
92
|
+
const MAX_AX_TOTAL_BYTES: usize = 1_200;
|
|
93
|
+
|
|
94
|
+
/// Keep text useful for scene continuity without retaining arbitrary field
|
|
95
|
+
/// contents. Password-like controls and credential-shaped values are omitted.
|
|
96
|
+
fn safe_ax_text(value: Option<String>, remaining: &mut usize) -> Option<String> {
|
|
97
|
+
let value = value?;
|
|
98
|
+
let value = value.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
99
|
+
if value.is_empty() || looks_like_credential(&value) {
|
|
100
|
+
return None;
|
|
101
|
+
}
|
|
102
|
+
let limit = MAX_AX_STRING_BYTES.min(*remaining);
|
|
103
|
+
if limit == 0 {
|
|
104
|
+
return None;
|
|
105
|
+
}
|
|
106
|
+
let mut end = value.len().min(limit);
|
|
107
|
+
while end > 0 && !value.is_char_boundary(end) {
|
|
108
|
+
end -= 1;
|
|
109
|
+
}
|
|
110
|
+
let value = value[..end].to_string();
|
|
111
|
+
if value.is_empty() {
|
|
112
|
+
return None;
|
|
113
|
+
}
|
|
114
|
+
*remaining = remaining.saturating_sub(value.len());
|
|
115
|
+
Some(value)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn looks_like_credential(value: &str) -> bool {
|
|
119
|
+
let lower = value.to_ascii_lowercase();
|
|
120
|
+
if [
|
|
121
|
+
"password",
|
|
122
|
+
"passcode",
|
|
123
|
+
"secret=",
|
|
124
|
+
"client secret",
|
|
125
|
+
"api key",
|
|
126
|
+
"api_key",
|
|
127
|
+
"access token",
|
|
128
|
+
"auth token",
|
|
129
|
+
"bearer ",
|
|
130
|
+
]
|
|
131
|
+
.iter()
|
|
132
|
+
.any(|marker| lower.contains(marker))
|
|
133
|
+
{
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
let compact = value
|
|
137
|
+
.chars()
|
|
138
|
+
.filter(|c| !c.is_whitespace())
|
|
139
|
+
.collect::<String>();
|
|
140
|
+
let tokenish = compact.len() >= 32
|
|
141
|
+
&& compact
|
|
142
|
+
.chars()
|
|
143
|
+
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'));
|
|
144
|
+
let jwtish = compact.matches('.').count() == 2 && compact.len() >= 32;
|
|
145
|
+
let private_key = value.contains("-----BEGIN") && value.contains("PRIVATE KEY-----");
|
|
146
|
+
tokenish || jwtish || private_key
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn role_is_secure(role: &str) -> bool {
|
|
150
|
+
let role = role.to_ascii_lowercase();
|
|
151
|
+
role.contains("secure") || role.contains("password")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn safe_ax_url(url: String, remaining: &mut usize) -> Option<String> {
|
|
155
|
+
let url = url.split(['?', '#']).next()?.to_string();
|
|
156
|
+
// Never retain a URL that embeds credentials in its authority component.
|
|
157
|
+
let authority = url
|
|
158
|
+
.split_once("://")?
|
|
159
|
+
.1
|
|
160
|
+
.split('/')
|
|
161
|
+
.next()
|
|
162
|
+
.unwrap_or_default();
|
|
163
|
+
if authority.contains('@') {
|
|
164
|
+
return None;
|
|
165
|
+
}
|
|
166
|
+
if url.split('/').skip(3).any(looks_like_credential) {
|
|
167
|
+
return None;
|
|
168
|
+
}
|
|
169
|
+
safe_ax_text(Some(url), remaining)
|
|
170
|
+
}
|
|
58
171
|
|
|
59
172
|
#[derive(Debug, Clone, Serialize)]
|
|
60
173
|
#[serde(rename_all = "camelCase")]
|
|
@@ -122,9 +235,53 @@ pub async fn accessibility_request_permission() -> Result<bool, String> {
|
|
|
122
235
|
}
|
|
123
236
|
}
|
|
124
237
|
|
|
238
|
+
#[cfg(test)]
|
|
239
|
+
mod tests {
|
|
240
|
+
use super::*;
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn ax_text_is_bounded_and_budgeted() {
|
|
244
|
+
let mut remaining = 12;
|
|
245
|
+
let value = safe_ax_text(Some("a very long accessible title".into()), &mut remaining)
|
|
246
|
+
.expect("safe title");
|
|
247
|
+
assert_eq!(value.len(), 12);
|
|
248
|
+
assert_eq!(remaining, 0);
|
|
249
|
+
assert!(safe_ax_text(Some("another title".into()), &mut remaining).is_none());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn credential_shaped_text_is_not_retained() {
|
|
254
|
+
let mut remaining = MAX_AX_TOTAL_BYTES;
|
|
255
|
+
assert!(safe_ax_text(Some("password: hunter2".into()), &mut remaining).is_none());
|
|
256
|
+
assert!(
|
|
257
|
+
safe_ax_text(Some("API key: short-but-sensitive".into()), &mut remaining).is_none()
|
|
258
|
+
);
|
|
259
|
+
assert!(safe_ax_text(
|
|
260
|
+
Some("eyJhbGciOiJIUzI1NiJ9.abcdefghijklmnopqrstuvwxyz.0123456789abcdef".into()),
|
|
261
|
+
&mut remaining
|
|
262
|
+
)
|
|
263
|
+
.is_none());
|
|
264
|
+
assert!(role_is_secure("AXSecureTextField"));
|
|
265
|
+
assert!(safe_ax_url(
|
|
266
|
+
"https://alice:secret@example.test/document".into(),
|
|
267
|
+
&mut remaining
|
|
268
|
+
)
|
|
269
|
+
.is_none());
|
|
270
|
+
assert!(safe_ax_url(
|
|
271
|
+
"https://example.test/session/abcdefghijklmnopqrstuvwxyz123456".into(),
|
|
272
|
+
&mut remaining
|
|
273
|
+
)
|
|
274
|
+
.is_none());
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
125
278
|
#[cfg(target_os = "macos")]
|
|
126
279
|
pub(crate) mod macos {
|
|
127
|
-
use super::
|
|
280
|
+
use super::{
|
|
281
|
+
role_is_secure, safe_ax_text, safe_ax_url, AccessibilityFingerprint,
|
|
282
|
+
AccessibilitySemanticNode, ActiveWindowContext, MAX_AX_DEPTH, MAX_AX_LABELS, MAX_AX_NODES,
|
|
283
|
+
MAX_AX_TOTAL_BYTES,
|
|
284
|
+
};
|
|
128
285
|
use core_foundation::array::CFArray;
|
|
129
286
|
use core_foundation::base::{CFType, TCFType};
|
|
130
287
|
use core_foundation::dictionary::CFDictionary;
|
|
@@ -140,6 +297,7 @@ pub(crate) mod macos {
|
|
|
140
297
|
type CFTypeRef = *const c_void;
|
|
141
298
|
type CFStringRef = *const c_void;
|
|
142
299
|
type CFDictionaryRef = *const c_void;
|
|
300
|
+
type CFArrayRef = *const c_void;
|
|
143
301
|
type CFBooleanRef = *const c_void;
|
|
144
302
|
type CFAllocatorRef = *const c_void;
|
|
145
303
|
type CFIndex = isize;
|
|
@@ -166,6 +324,7 @@ pub(crate) mod macos {
|
|
|
166
324
|
extern "C" {
|
|
167
325
|
fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> Boolean;
|
|
168
326
|
fn AXUIElementCreateSystemWide() -> AXUIElementRef;
|
|
327
|
+
fn AXUIElementCreateApplication(pid: i32) -> AXUIElementRef;
|
|
169
328
|
fn AXUIElementCopyAttributeValue(
|
|
170
329
|
element: AXUIElementRef,
|
|
171
330
|
attribute: CFStringRef,
|
|
@@ -194,6 +353,10 @@ pub(crate) mod macos {
|
|
|
194
353
|
fn CFRelease(cf: CFTypeRef);
|
|
195
354
|
fn CFGetTypeID(cf: CFTypeRef) -> CFTypeID;
|
|
196
355
|
fn CFStringGetTypeID() -> CFTypeID;
|
|
356
|
+
fn CFURLGetTypeID() -> CFTypeID;
|
|
357
|
+
fn CFArrayGetCount(array: CFArrayRef) -> CFIndex;
|
|
358
|
+
fn CFArrayGetValueAtIndex(array: CFArrayRef, index: CFIndex) -> *const c_void;
|
|
359
|
+
fn CFURLGetString(url: CFTypeRef) -> CFStringRef;
|
|
197
360
|
fn CFDictionaryCreate(
|
|
198
361
|
allocator: CFAllocatorRef,
|
|
199
362
|
keys: *const *const c_void,
|
|
@@ -341,25 +504,214 @@ pub(crate) mod macos {
|
|
|
341
504
|
}
|
|
342
505
|
}
|
|
343
506
|
|
|
507
|
+
/// Collect the semantic surface from the same application selected by the
|
|
508
|
+
/// Core Graphics foreground-window sample. This avoids combining an
|
|
509
|
+
/// ignored Clips utility window's AX tree with the underlying work app.
|
|
510
|
+
pub fn semantic_fingerprint_for_pid_impl(pid: i32) -> Option<AccessibilityFingerprint> {
|
|
511
|
+
unsafe {
|
|
512
|
+
if !is_trusted(false) {
|
|
513
|
+
return None;
|
|
514
|
+
}
|
|
515
|
+
let application = AXUIElementCreateApplication(pid);
|
|
516
|
+
if application.is_null() {
|
|
517
|
+
return None;
|
|
518
|
+
}
|
|
519
|
+
let window = copy_ax_element_attribute(application, "AXFocusedWindow");
|
|
520
|
+
let focused = copy_ax_element_attribute(application, "AXFocusedUIElement");
|
|
521
|
+
CFRelease(application as CFTypeRef);
|
|
522
|
+
|
|
523
|
+
let document_element = window.or(focused)?;
|
|
524
|
+
let mut remaining = MAX_AX_TOTAL_BYTES;
|
|
525
|
+
let document = semantic_node(document_element, false, &mut remaining);
|
|
526
|
+
let focused_node =
|
|
527
|
+
focused.and_then(|element| semantic_node(element, true, &mut remaining));
|
|
528
|
+
let mut labels = Vec::new();
|
|
529
|
+
collect_visible_labels(document_element, 0, &mut labels, &mut remaining);
|
|
530
|
+
|
|
531
|
+
if let Some(window) = window {
|
|
532
|
+
CFRelease(window);
|
|
533
|
+
}
|
|
534
|
+
if let Some(focused) = focused {
|
|
535
|
+
CFRelease(focused);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if document.is_none() && focused_node.is_none() && labels.is_empty() {
|
|
539
|
+
None
|
|
540
|
+
} else {
|
|
541
|
+
Some(AccessibilityFingerprint {
|
|
542
|
+
document,
|
|
543
|
+
focused: focused_node,
|
|
544
|
+
visible_labels: labels,
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
unsafe fn copy_ax_element_attribute(
|
|
551
|
+
element: AXUIElementRef,
|
|
552
|
+
attribute: &str,
|
|
553
|
+
) -> Option<AXUIElementRef> {
|
|
554
|
+
let attribute = cfstr(attribute);
|
|
555
|
+
if attribute.is_null() {
|
|
556
|
+
return None;
|
|
557
|
+
}
|
|
558
|
+
let mut value = ptr::null();
|
|
559
|
+
let error = AXUIElementCopyAttributeValue(element, attribute, &mut value);
|
|
560
|
+
CFRelease(attribute as CFTypeRef);
|
|
561
|
+
(error == AX_ERROR_SUCCESS && !value.is_null()).then_some(value as AXUIElementRef)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
unsafe fn ax_string_attribute(element: AXUIElementRef, attribute: &str) -> Option<String> {
|
|
565
|
+
let attribute = cfstr(attribute);
|
|
566
|
+
if attribute.is_null() {
|
|
567
|
+
return None;
|
|
568
|
+
}
|
|
569
|
+
let mut raw_value = ptr::null();
|
|
570
|
+
let error = AXUIElementCopyAttributeValue(element, attribute, &mut raw_value);
|
|
571
|
+
CFRelease(attribute as CFTypeRef);
|
|
572
|
+
if error != AX_ERROR_SUCCESS || raw_value.is_null() {
|
|
573
|
+
return None;
|
|
574
|
+
}
|
|
575
|
+
let value = if CFGetTypeID(raw_value) == CFStringGetTypeID() {
|
|
576
|
+
cfstring_to_string(raw_value as CFStringRef)
|
|
577
|
+
} else {
|
|
578
|
+
None
|
|
579
|
+
};
|
|
580
|
+
CFRelease(raw_value);
|
|
581
|
+
value
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
unsafe fn ax_url_attribute(element: AXUIElementRef) -> Option<String> {
|
|
585
|
+
let attribute = cfstr("AXURL");
|
|
586
|
+
if attribute.is_null() {
|
|
587
|
+
return None;
|
|
588
|
+
}
|
|
589
|
+
let mut value = ptr::null();
|
|
590
|
+
let error = AXUIElementCopyAttributeValue(element, attribute, &mut value);
|
|
591
|
+
CFRelease(attribute as CFTypeRef);
|
|
592
|
+
if error != AX_ERROR_SUCCESS || value.is_null() {
|
|
593
|
+
return None;
|
|
594
|
+
}
|
|
595
|
+
// URLs may contain query-string credentials. Keep only the stable
|
|
596
|
+
// origin/path portion, and omit anything that cannot be read safely.
|
|
597
|
+
let string = if CFGetTypeID(value) == CFStringGetTypeID() {
|
|
598
|
+
value as CFStringRef
|
|
599
|
+
} else if CFGetTypeID(value) == CFURLGetTypeID() {
|
|
600
|
+
CFURLGetString(value)
|
|
601
|
+
} else {
|
|
602
|
+
ptr::null()
|
|
603
|
+
};
|
|
604
|
+
let result = cfstring_to_string(string)
|
|
605
|
+
.and_then(|url| url.split(['?', '#']).next().map(str::to_string));
|
|
606
|
+
CFRelease(value);
|
|
607
|
+
result
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
unsafe fn semantic_node(
|
|
611
|
+
element: AXUIElementRef,
|
|
612
|
+
focused: bool,
|
|
613
|
+
remaining: &mut usize,
|
|
614
|
+
) -> Option<AccessibilitySemanticNode> {
|
|
615
|
+
let role = ax_string_attribute(element, "AXRole")?;
|
|
616
|
+
if role_is_secure(&role) {
|
|
617
|
+
return None;
|
|
618
|
+
}
|
|
619
|
+
let role = safe_ax_text(Some(role), remaining)?;
|
|
620
|
+
let title = safe_ax_text(ax_string_attribute(element, "AXTitle"), remaining);
|
|
621
|
+
let description = safe_ax_text(ax_string_attribute(element, "AXDescription"), remaining);
|
|
622
|
+
let document = safe_ax_text(ax_string_attribute(element, "AXDocument"), remaining);
|
|
623
|
+
let url = ax_url_attribute(element).and_then(|url| safe_ax_url(url, remaining));
|
|
624
|
+
let selected_text = focused
|
|
625
|
+
.then(|| ax_string_attribute(element, "AXSelectedText"))
|
|
626
|
+
.flatten()
|
|
627
|
+
.and_then(|text| safe_ax_text(Some(text), remaining));
|
|
628
|
+
Some(AccessibilitySemanticNode {
|
|
629
|
+
role,
|
|
630
|
+
title,
|
|
631
|
+
description,
|
|
632
|
+
document,
|
|
633
|
+
url,
|
|
634
|
+
selected_text,
|
|
635
|
+
})
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
unsafe fn collect_visible_labels(
|
|
639
|
+
element: AXUIElementRef,
|
|
640
|
+
depth: usize,
|
|
641
|
+
labels: &mut Vec<String>,
|
|
642
|
+
remaining: &mut usize,
|
|
643
|
+
) {
|
|
644
|
+
if depth >= MAX_AX_DEPTH || labels.len() >= MAX_AX_LABELS || *remaining == 0 {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
let attribute = cfstr("AXChildren");
|
|
648
|
+
if attribute.is_null() {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
let mut children = ptr::null();
|
|
652
|
+
let error = AXUIElementCopyAttributeValue(element, attribute, &mut children);
|
|
653
|
+
CFRelease(attribute as CFTypeRef);
|
|
654
|
+
if error != AX_ERROR_SUCCESS || children.is_null() {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
let count = (CFArrayGetCount(children) as usize).min(MAX_AX_NODES);
|
|
658
|
+
for index in 0..count {
|
|
659
|
+
if labels.len() >= MAX_AX_LABELS || *remaining == 0 {
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
let child = CFArrayGetValueAtIndex(children, index as CFIndex) as AXUIElementRef;
|
|
663
|
+
if child.is_null() {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
let role = ax_string_attribute(child, "AXRole");
|
|
667
|
+
if role.as_deref().is_some_and(role_is_secure) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
let label = ax_string_attribute(child, "AXTitle")
|
|
671
|
+
.or_else(|| ax_string_attribute(child, "AXDescription"))
|
|
672
|
+
.and_then(|text| safe_ax_text(Some(text), remaining));
|
|
673
|
+
if let Some(label) = label {
|
|
674
|
+
labels.push(label);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
CFRelease(children);
|
|
678
|
+
}
|
|
679
|
+
|
|
344
680
|
pub fn active_window_context_impl() -> ActiveWindowContext {
|
|
681
|
+
active_window_context_with_pid_impl().0
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
pub(crate) fn active_window_context_with_pid_impl() -> (ActiveWindowContext, Option<i32>) {
|
|
345
685
|
let window = active_window_from_core_graphics();
|
|
346
686
|
let frontmost = frontmost_application();
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
687
|
+
let owner = window
|
|
688
|
+
.as_ref()
|
|
689
|
+
.and_then(|window| window.owner_pid)
|
|
690
|
+
.and_then(application_for_pid);
|
|
691
|
+
let owner_pid = window.as_ref().and_then(|window| window.owner_pid);
|
|
692
|
+
|
|
693
|
+
(
|
|
694
|
+
ActiveWindowContext {
|
|
695
|
+
app_name: window
|
|
696
|
+
.as_ref()
|
|
697
|
+
.and_then(|w| w.app_name.clone())
|
|
698
|
+
.or_else(|| owner.as_ref().and_then(|app| app.app_name.clone()))
|
|
699
|
+
.or_else(|| frontmost.as_ref().and_then(|app| app.app_name.clone())),
|
|
700
|
+
window_title: window.and_then(|w| w.window_title),
|
|
701
|
+
bundle_id: owner
|
|
702
|
+
.and_then(|app| app.bundle_id)
|
|
703
|
+
.or_else(|| frontmost.and_then(|app| app.bundle_id)),
|
|
704
|
+
source: "core-graphics".to_string(),
|
|
705
|
+
},
|
|
706
|
+
owner_pid,
|
|
707
|
+
)
|
|
357
708
|
}
|
|
358
709
|
|
|
359
710
|
#[derive(Debug, Clone)]
|
|
360
711
|
struct WindowInfo {
|
|
361
712
|
app_name: Option<String>,
|
|
362
713
|
window_title: Option<String>,
|
|
714
|
+
owner_pid: Option<i32>,
|
|
363
715
|
}
|
|
364
716
|
|
|
365
717
|
#[derive(Debug, Clone)]
|
|
@@ -400,6 +752,8 @@ pub(crate) mod macos {
|
|
|
400
752
|
return Some(WindowInfo {
|
|
401
753
|
app_name: owner,
|
|
402
754
|
window_title: title,
|
|
755
|
+
owner_pid: dict_number_i64(&window, "kCGWindowOwnerPID")
|
|
756
|
+
.and_then(|pid| i32::try_from(pid).ok()),
|
|
403
757
|
});
|
|
404
758
|
}
|
|
405
759
|
}
|
|
@@ -430,6 +784,26 @@ pub(crate) mod macos {
|
|
|
430
784
|
}
|
|
431
785
|
}
|
|
432
786
|
|
|
787
|
+
fn application_for_pid(pid: i32) -> Option<AppInfo> {
|
|
788
|
+
use objc2::msg_send;
|
|
789
|
+
use objc2::runtime::{AnyClass, AnyObject};
|
|
790
|
+
|
|
791
|
+
unsafe {
|
|
792
|
+
let class_name = std::ffi::CString::new("NSRunningApplication").ok()?;
|
|
793
|
+
let cls: &AnyClass = AnyClass::get(&class_name)?;
|
|
794
|
+
let app: *mut AnyObject = msg_send![cls, runningApplicationWithProcessIdentifier: pid];
|
|
795
|
+
if app.is_null() {
|
|
796
|
+
return None;
|
|
797
|
+
}
|
|
798
|
+
let app_name: *mut AnyObject = msg_send![app, localizedName];
|
|
799
|
+
let bundle_id: *mut AnyObject = msg_send![app, bundleIdentifier];
|
|
800
|
+
Some(AppInfo {
|
|
801
|
+
app_name: ns_string_to_owned(app_name),
|
|
802
|
+
bundle_id: ns_string_to_owned(bundle_id),
|
|
803
|
+
})
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
433
807
|
fn dict_string(dict: &CFDictionary<CFString, CFType>, key: &'static str) -> Option<String> {
|
|
434
808
|
let key = CFString::from_static_string(key);
|
|
435
809
|
let value = dict.find(&key)?;
|