Mac permissions: ask at most every ten minutes; one fixed signing key keeps them across updates; open apps by their shown name
- Every untrusted step popped macOS's prompt and opened Settings; a remote brain's steps covered the screen. Now once per pane per ten minutes. - Ad hoc signing made each build a new app to macOS, so 輔助使用/螢幕錄製 silently stopped working after updates (agent-desktop PERM_DENIED). deploy/mac/sign.sh signs with a local key kept in its own keychain under AnyBot's folder. - computer_use_open_app finds 「計算機」 by display name and says the app opened even when its window can't be read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b0a0babf1
commit
5b2c974f3c
|
|
@ -2,7 +2,7 @@
|
|||
# Make AnyBot.app from the release build and zip it (run from app/ on a Mac, after
|
||||
# `cargo build --release`): deploy/mac/bundle.sh 1.2.0 → dist/AnyBot-1.2.0-macos-<arch>.zip
|
||||
# With KEEP_APP=1 it leaves dist/AnyBot.app instead of zipping it (make install).
|
||||
# Signed ad hoc (no Apple developer account): the first time, right-click › 打開.
|
||||
# Signed with this Mac's own fixed key (no Apple developer account): the first time, right-click › 打開.
|
||||
set -e
|
||||
VERSION=${1:?version}
|
||||
BIN=${CARGO_TARGET_DIR:-target}/release/anybot
|
||||
|
|
@ -39,7 +39,8 @@ cat > "$APP/Contents/Info.plist" <<PLIST
|
|||
<key>NSAppleEventsUsageDescription</key><string>讓 Bot 在你允許時操作其他 App。</string>
|
||||
</dict></plist>
|
||||
PLIST
|
||||
codesign --force --deep -s - "$APP"
|
||||
# The same signature every build, so macOS keeps the permissions it gave (see sign.sh).
|
||||
sh "$(dirname "$0")/sign.sh" "$APP"
|
||||
if [ "${KEEP_APP:-}" = 1 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
#!/bin/sh
|
||||
# Sign AnyBot.app with this Mac's own signing key, the same one every build:
|
||||
# deploy/mac/sign.sh dist/AnyBot.app
|
||||
# macOS keeps 輔助使用 / 螢幕錄製 for an app by its signature. Signed "ad hoc", every build was a
|
||||
# new app to it, so each update quietly lost those permissions and the bots could no longer read
|
||||
# or operate other apps (agent-desktop answered PERM_DENIED). One fixed self-signed key, kept in
|
||||
# its own keychain under AnyBot's folder (the login keychain is not touched), keeps them.
|
||||
# Anything going wrong falls back to ad hoc signing.
|
||||
set -u
|
||||
APP=${1:?app}
|
||||
DIR="$HOME/Library/Application Support/AnyBot/signing"
|
||||
KC="$DIR/anybot-signing.keychain-db"
|
||||
PWF="$DIR/keychain-password"
|
||||
NAME="AnyBot Local Signing"
|
||||
|
||||
make_key() {
|
||||
mkdir -p "$DIR" && chmod 700 "$DIR" || return 1
|
||||
head -c 24 /dev/urandom | base64 > "$PWF" && chmod 600 "$PWF" || return 1
|
||||
PW=$(cat "$PWF")
|
||||
T=$(mktemp -d)
|
||||
cat > "$T/cert.cnf" <<CNF
|
||||
[req]
|
||||
distinguished_name=dn
|
||||
x509_extensions=ext
|
||||
prompt=no
|
||||
[dn]
|
||||
CN=$NAME
|
||||
[ext]
|
||||
keyUsage=critical,digitalSignature
|
||||
extendedKeyUsage=critical,codeSigning
|
||||
basicConstraints=critical,CA:false
|
||||
CNF
|
||||
ok=1
|
||||
/usr/bin/openssl req -x509 -newkey rsa:2048 -nodes -keyout "$T/key.pem" -out "$T/cert.pem" -days 7300 -config "$T/cert.cnf" >/dev/null 2>&1 \
|
||||
&& /usr/bin/openssl pkcs12 -export -inkey "$T/key.pem" -in "$T/cert.pem" -out "$T/id.p12" -passout pass:"$PW" >/dev/null 2>&1 \
|
||||
&& security create-keychain -p "$PW" "$KC" >/dev/null \
|
||||
&& security set-keychain-settings "$KC" \
|
||||
&& security unlock-keychain -p "$PW" "$KC" \
|
||||
&& security import "$T/id.p12" -k "$KC" -P "$PW" -T /usr/bin/codesign >/dev/null \
|
||||
&& security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$PW" "$KC" >/dev/null 2>&1 \
|
||||
&& ok=0
|
||||
rm -rf "$T"
|
||||
return $ok
|
||||
}
|
||||
|
||||
signed=1
|
||||
if [ -f "$KC" ] || make_key; then
|
||||
if security unlock-keychain -p "$(cat "$PWF")" "$KC" 2>/dev/null \
|
||||
&& codesign --force --deep --keychain "$KC" -s "$NAME" "$APP" 2>/dev/null; then
|
||||
signed=0
|
||||
echo "已用這台 Mac 固定的簽章簽好(更新後「輔助使用」「螢幕錄製」權限會保留)"
|
||||
fi
|
||||
fi
|
||||
if [ $signed -ne 0 ]; then
|
||||
codesign --force --deep -s - "$APP"
|
||||
echo "注意:用臨時簽章(更新後要重新允許「輔助使用」「螢幕錄製」)"
|
||||
fi
|
||||
|
|
@ -303,8 +303,8 @@ pub fn read_person_browser(exe: &str, max_chars: usize) -> Result<Page, String>
|
|||
if !cfg!(target_os = "macos") {
|
||||
return Err("只支援 macOS".into());
|
||||
}
|
||||
if !trusted(true) {
|
||||
let _ = crate::computer::spawn_reaped(&mut std::process::Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"));
|
||||
if !trusted(false) {
|
||||
crate::computer::ask_permission("Privacy_Accessibility");
|
||||
return Err("macOS 還沒讓 AnyBot 使用「輔助使用」,所以看不到你的瀏覽器。我已經打開「系統設定 → 隱私權與安全性 → 輔助使用」:把 AnyBot(或「終端機」)打開,然後再叫我試一次。只要開一次。".into());
|
||||
}
|
||||
let ps = std::process::Command::new("ps").args(["-axo", "pid=,args="]).output().map(|o| String::from_utf8_lossy(&o.stdout).into_owned()).unwrap_or_default();
|
||||
|
|
|
|||
|
|
@ -394,6 +394,53 @@ fn run_text_mode_name(name: &str, v: &Value) -> Option<()> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ask macOS for a permission (its own prompt, then 系統設定 at that pane) at most once every ten
|
||||
/// minutes. Asking on every step meant a bot working through several steps — or a brain far away
|
||||
/// working on this computer — covered the screen with prompts and Settings windows, and the
|
||||
/// person could do nothing else.
|
||||
pub fn ask_permission(pane: &str) {
|
||||
static LAST: std::sync::Mutex<Vec<(String, std::time::Instant)>> = std::sync::Mutex::new(Vec::new());
|
||||
{
|
||||
let mut l = LAST.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if l.iter().any(|(p, t)| p == pane && t.elapsed() < std::time::Duration::from_secs(600)) {
|
||||
return;
|
||||
}
|
||||
l.retain(|(p, _)| p != pane);
|
||||
l.push((pane.to_string(), std::time::Instant::now()));
|
||||
}
|
||||
if !cfg!(target_os = "macos") {
|
||||
return;
|
||||
}
|
||||
match pane {
|
||||
"Privacy_Accessibility" => {
|
||||
let _ = crate::axread::trusted(true);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
"Privacy_ScreenCapture" => {
|
||||
let _ = mac::screen_capture_allowed();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let _ = spawn_reaped(&mut std::process::Command::new("open").arg(format!("x-apple.systempreferences:com.apple.preference.security?{pane}")));
|
||||
}
|
||||
|
||||
/// The app a shown name stands for (「計算機」 → Calculator), from the apps' display names.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn app_named(name: &str) -> Option<String> {
|
||||
if name.is_empty() || name.contains(['"', '\\', '\'']) {
|
||||
return None;
|
||||
}
|
||||
let q = format!("kMDItemDisplayName == \"{name}\"cd && kMDItemContentType == \"com.apple.application-bundle\"");
|
||||
let out = std::process::Command::new("mdfind").arg(q).output().ok()?;
|
||||
let path = String::from_utf8_lossy(&out.stdout).lines().next()?.trim().to_string();
|
||||
std::path::Path::new(&path).file_stem().map(|s| s.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn app_named(_name: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The accessibility (text) way: agent-desktop, by element number, in the background.
|
||||
fn run_text_mode(name: &str, v: &Value) -> Option<Result<(String, Option<String>), String>> {
|
||||
let rf = v["ref"].as_str().unwrap_or("").trim().to_string();
|
||||
|
|
@ -407,9 +454,20 @@ fn run_text_mode(name: &str, v: &Value) -> Option<Result<(String, Option<String>
|
|||
}),
|
||||
"computer_use_open_app" => {
|
||||
let app = v["app"].as_str().unwrap_or("").trim().to_string();
|
||||
crate::desktop::run("launch", &json!({"app": app})).and_then(|_| {
|
||||
// A name as the person sees it (「計算機」 on a Chinese Mac) isn't one the system
|
||||
// launches by; find the app it is and launch that.
|
||||
let launched = crate::desktop::run("launch", &json!({"app": app})).or_else(|e| match app_named(&app) {
|
||||
Some(real) if real != app => crate::desktop::run("launch", &json!({"app": real})),
|
||||
_ => Err(e),
|
||||
});
|
||||
launched.map(|_| {
|
||||
std::thread::sleep(std::time::Duration::from_millis(800));
|
||||
crate::desktop::read_app(&app).map(|(a, lines)| format!("已打開「{a}」。\n{}", lines.join("\n")))
|
||||
// Open is open, even when its window can't be read (that's a separate problem,
|
||||
// and the bot said "couldn't open" while the app sat there on the screen).
|
||||
match crate::desktop::read_app(&app) {
|
||||
Ok((a, lines)) => format!("已打開「{a}」。\n{}", lines.join("\n")),
|
||||
Err(e) => format!("已打開「{app}」,但讀不到它的畫面:{e}"),
|
||||
}
|
||||
})
|
||||
}
|
||||
"computer_use_select" => crate::desktop::act("select", &rf, &json!({"value": v["value"]})),
|
||||
|
|
@ -464,8 +522,8 @@ fn run_inner(name: &str, v: &Value) -> Result<(String, Option<String>), String>
|
|||
if HELD_BY_PERSON.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err("使用者正在自己操作這台電腦(已取得控制權)。請等他交還控制權後再操作;可以先截圖看看。".into());
|
||||
}
|
||||
if cfg!(target_os = "macos") && !crate::axread::trusted(true) {
|
||||
let _ = spawn_reaped(&mut std::process::Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"));
|
||||
if cfg!(target_os = "macos") && !crate::axread::trusted(false) {
|
||||
ask_permission("Privacy_Accessibility");
|
||||
return Err("macOS 還沒讓 AnyBot 使用「輔助使用」,所以不能動滑鼠和鍵盤。我已經打開「系統設定 → 隱私權與安全性 → 輔助使用」:把 AnyBot(或「終端機」)打開,再叫我試一次。只要開一次。".into());
|
||||
}
|
||||
let scale = || SCALE.lock().unwrap_or_else(|e| e.into_inner()).ok_or_else(|| "請先用 computer_use_screenshot 看一下畫面,座標要用截圖上的位置。".to_string());
|
||||
|
|
@ -631,8 +689,8 @@ pub fn app_at_point(x: f64, y: f64) -> Option<String> {
|
|||
}
|
||||
|
||||
fn screenshot() -> Result<(String, String), String> {
|
||||
if !mac::screen_capture_allowed() {
|
||||
let _ = spawn_reaped(&mut std::process::Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"));
|
||||
if !mac::screen_capture_preflight() {
|
||||
ask_permission("Privacy_ScreenCapture");
|
||||
return Err("macOS 還沒讓 AnyBot 錄製螢幕,所以看不到畫面。我已經打開「系統設定 → 隱私權與安全性 → 螢幕與系統錄音」:把 AnyBot(或「終端機」)打開,再叫我試一次。只要開一次。".into());
|
||||
}
|
||||
let img = capture_screen()?;
|
||||
|
|
|
|||
Loading…
Reference in New Issue