AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
// What a bot may do on this computer: list, search, read and edit files, run commands
// (long ones move to the background), and the rules that decide when to ask first.
//
// Limits come from the original's local tool runner (dist/local-exec-daemon/main.cjs):
// search: MAIN_TIMEOUT_MS=25e3, HARD_MAX_OUTPUT_LINES=1e4 (@2983388), client limit Jut=2e3 (@2966784)
// read: at most 8388608 bytes (gut, @2951190)
// shell: on timeout a background-able command keeps running and is handed off (Mye, @3010873/@3016213)
// Permission order follows the neighbouring investigation's design notes (fnd_8):
// deny > ignore > allow > ask, with chained shell commands split into parts first.
use serde_json ::{ json , Value } ;
use std ::collections ::HashMap ;
use std ::io ::Read ;
use std ::path ::{ Path , PathBuf } ;
use std ::process ::{ Command , Stdio } ;
use std ::sync ::atomic ::{ AtomicBool , Ordering } ;
use std ::sync ::{ Arc , Mutex , OnceLock } ;
use std ::time ::{ Duration , Instant } ;
pub const SEARCH_TIMEOUT_MS : u64 = 25_000 ;
pub const SEARCH_HARD_MAX_LINES : usize = 10_000 ;
pub const SEARCH_CLIENT_LIMIT_LINES : usize = 2_000 ;
pub const READ_MAX_BYTES : u64 = 8_388_608 ;
/// Lines returned by one read_file call when the model gives no limit (estimated).
pub const READ_DEFAULT_LINES : usize = 2_000 ;
/// Foreground time before a command is moved to the background (estimated; the original's comes per call).
pub const SHELL_DEFAULT_TIMEOUT_MS : u64 = 30_000 ;
pub const SHELL_MAX_TIMEOUT_MS : u64 = 600_000 ;
2026-11-05 13:28:32 +00:00
/// A command on a relayed computer must answer before the relay's forward timeout (35 s):
/// on the device it moves to the background by this deadline, and read_terminal picks it up.
pub const DEVICE_SHELL_TIMEOUT_MS : u64 = 25_000 ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
/// Output kept from a command: its start and its end, with the middle left out (sizes estimated;
/// the original reports output_head / output_tail / elided_chars).
pub const OUTPUT_HEAD_CHARS : usize = 6_000 ;
pub const OUTPUT_TAIL_CHARS : usize = 6_000 ;
/// Names the model sees, with JSON-schema parameters.
pub fn specs ( ) -> Vec < Value > {
vec! [
json! ( { " name " : " list_dir " , " description " : " List the files and folders in a folder (relative to the work folder unless absolute). " ,
" parameters " : { " type " : " object " , " properties " : { " path " : { " type " : " string " , " description " : " Folder to list; empty means the work folder. " } } , " required " : [ ] } } ) ,
json! ( { " name " : " search " , " description " : " Find text in files under a folder. Returns matching lines as path:line:text. " ,
" parameters " : { " type " : " object " , " properties " : {
" pattern " : { " type " : " string " , " description " : " Text to look for (regular expression when ripgrep is installed). " } ,
" path " : { " type " : " string " , " description " : " Folder or file to search; empty means the work folder. " } ,
" files_only " : { " type " : " boolean " , " description " : " Only list the files that match. " } } , " required " : [ " pattern " ] } } ) ,
json! ( { " name " : " read_file " , " description " : " Read a text file. Lines come back numbered. " ,
" parameters " : { " type " : " object " , " properties " : {
" path " : { " type " : " string " } ,
" offset " : { " type " : " integer " , " description " : " First line to read (1-based). " } ,
" limit " : { " type " : " integer " , " description " : " How many lines to read. " } } , " required " : [ " path " ] } } ) ,
json! ( { " name " : " edit_file " , " description " : " Change a file. Replaces old_text (which must appear exactly once) with new_text. With empty old_text the file is created or overwritten with new_text. " ,
" parameters " : { " type " : " object " , " properties " : {
" path " : { " type " : " string " } , " old_text " : { " type " : " string " } , " new_text " : { " type " : " string " } } , " required " : [ " path " , " new_text " ] } } ) ,
2026-11-05 13:28:32 +00:00
json! ( { " name " : " shell " , " description " : " Run a shell command in the work folder. If it runs longer than timeout_ms it keeps running in the background and you get a terminal id to check with read_terminal (pass the same computer to read_terminal/write_terminal). " ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
" parameters " : { " type " : " object " , " properties " : {
" command " : { " type " : " string " } ,
" timeout_ms " : { " type " : " integer " , " description " : " How long to wait before moving it to the background (default 30000). " } } , " required " : [ " command " ] } } ) ,
json! ( { " name " : " read_terminal " , " description " : " Read the latest output of a command that was moved to the background. " ,
" parameters " : { " type " : " object " , " properties " : { " id " : { " type " : " integer " } } , " required " : [ " id " ] } } ) ,
json! ( { " name " : " write_terminal " , " description " : " Send input to a command running in the background, as if typed into its terminal (e.g. answer a prompt: \" y \\ n \" ). close=true ends its input (like Ctrl-D). " ,
" parameters " : { " type " : " object " , " properties " : { " id " : { " type " : " integer " } , " input " : { " type " : " string " , " description " : " Text to send; include \\ n to press return. " } , " close " : { " type " : " boolean " } } , " required " : [ " id " ] } } ) ,
]
}
2026-11-05 09:29:55 +00:00
/// 請你接手: the step that asks the person to do something by hand on a computer.
pub const HANDOFF_TOOL : & str = " hand_over_computer " ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
pub fn label ( name : & str ) -> & 'static str {
match name {
2026-11-05 09:29:55 +00:00
HANDOFF_TOOL = > " 請你接手電腦 " ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
" list_dir " = > " 查看資料夾 " ,
" search " = > " 搜尋檔案 " ,
" read_file " = > " 讀取檔案 " ,
" edit_file " = > " 修改檔案 " ,
" shell " = > " 執行指令 " ,
" read_terminal " = > " 查看背景指令 " ,
" write_terminal " = > " 輸入到背景指令 " ,
" open_for_person " = > " 打開給你看 " ,
2026-11-06 11:55:40 +00:00
" send_file " = > " 傳檔案給你 " ,
" react " = > " 按表情 " ,
" view_demo_step " = > " 看示範 " ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
" publish_site " = > " 發布網站 " ,
" place_call " = > " 打電話 " ,
" send_sms " = > " 傳簡訊 " ,
" post_to_x " = > " 在 X 發文 " ,
" show_card " = > " 顯示卡片 " ,
" vault_list " = > " 查看保險庫 " ,
" browser_fill_login " = > " 用保險庫登入 " ,
" browser_fill_card " = > " 填入卡片 " ,
" browser_fill_contact " = > " 填入聯絡資訊 " ,
_ = > " 工具 " ,
}
}
Remove what nothing uses and keep one copy of what was written several times
Unused (found by taking away the dead-code allowances; nothing else used them, tests aside):
computer::specs, quiet_button, the never-made Poke::Spin, ten icons and their SVGs, the
theme's chrome/bubble_agent colours, ImportRow.line, voice's multipart/transcribe/speak
wrappers, and the old keychain writers vault::save/remove/index_add with their live test
(the vault store replaced them; reading old keychain entries for the import stays).
One copy each: percent-encoding (httpd::url_encode, was four), tool arguments (tools::args_of,
was three), the WebSocket handshake (httpd::accept_websocket, was three), the read pacing
(wire::Pace, was two: relay and app), docker with input and a time limit (dk_with_input, was
two). Docs: FEATURES §看畫面 (VNC), protocol (v* frames, /screen, tickets), architecture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:21:47 +00:00
/// A tool call's arguments (none = an empty object).
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
pub fn args_of ( args : & str ) -> Value {
Remove what nothing uses and keep one copy of what was written several times
Unused (found by taking away the dead-code allowances; nothing else used them, tests aside):
computer::specs, quiet_button, the never-made Poke::Spin, ten icons and their SVGs, the
theme's chrome/bubble_agent colours, ImportRow.line, voice's multipart/transcribe/speak
wrappers, and the old keychain writers vault::save/remove/index_add with their live test
(the vault store replaced them; reading old keychain entries for the import stays).
One copy each: percent-encoding (httpd::url_encode, was four), tool arguments (tools::args_of,
was three), the WebSocket handshake (httpd::accept_websocket, was three), the read pacing
(wire::Pace, was two: relay and app), docker with input and a time limit (dk_with_input, was
two). Docs: FEATURES §看畫面 (VNC), protocol (v* frames, /screen, tickets), architecture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:21:47 +00:00
serde_json ::from_str ( if args . trim ( ) . is_empty ( ) { " {} " } else { args } ) . unwrap_or ( Value ::Null )
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
fn arg_str ( v : & Value , k : & str ) -> String {
v . get ( k ) . and_then ( Value ::as_str ) . unwrap_or ( " " ) . to_string ( )
}
/// One short line describing a call, for the chat and the approval card.
pub fn describe ( name : & str , args : & str ) -> String {
let v = args_of ( args ) ;
match name {
" shell " = > arg_str ( & v , " command " ) ,
" search " = > format! ( " 「 {} 」 {} " , arg_str ( & v , " pattern " ) , arg_str ( & v , " path " ) ) . trim ( ) . to_string ( ) ,
" read_terminal " = > format! ( " 背景指令 # {} " , v . get ( " id " ) . and_then ( Value ::as_u64 ) . unwrap_or ( 0 ) ) ,
" write_terminal " = > {
let input : String = arg_str ( & v , " input " ) . chars ( ) . take ( 40 ) . collect ( ) ;
format! ( " 背景指令 # {} : {} {} " , v . get ( " id " ) . and_then ( Value ::as_u64 ) . unwrap_or ( 0 ) , input . escape_debug ( ) , if v . get ( " close " ) . and_then ( Value ::as_bool ) . unwrap_or ( false ) { " (結束輸入) " } else { " " } )
}
" vault_list " = > match arg_str ( & v , " kind " ) . as_str ( ) {
" login " = > " 登入資訊 " . into ( ) ,
" card " = > " 卡片 " . into ( ) ,
" contact " = > " 聯絡資訊 " . into ( ) ,
_ = > " 全部項目 " . into ( ) ,
} ,
" browser_fill_login " = > {
let site = arg_str ( & v , " site " ) ;
if site . is_empty ( ) { " 目前的網站 " . into ( ) } else { site }
}
" browser_fill_card " | " browser_fill_contact " = > " 目前的網頁 " . into ( ) ,
_ = > {
let p = arg_str ( & v , " path " ) ;
if p . is_empty ( ) { " 工作資料夾 " . into ( ) } else { p }
}
}
}
// ---------- paths ----------
pub fn expand_home ( p : & str ) -> PathBuf {
if let Some ( rest ) = p . strip_prefix ( " ~/ " ) {
if let Some ( h ) = dirs ::home_dir ( ) {
return h . join ( rest ) ;
}
} else if p = = " ~ " {
if let Some ( h ) = dirs ::home_dir ( ) {
return h ;
}
}
PathBuf ::from ( p )
}
/// Make a path absolute and tidy (no `.` or `..`) without touching the disk.
pub fn resolve ( workdir : & Path , p : & str ) -> PathBuf {
let raw = if p . trim ( ) . is_empty ( ) { workdir . to_path_buf ( ) } else {
let e = expand_home ( p . trim ( ) ) ;
if e . is_absolute ( ) { e } else { workdir . join ( e ) }
} ;
let mut out = PathBuf ::new ( ) ;
for c in raw . components ( ) {
match c {
std ::path ::Component ::ParentDir = > {
out . pop ( ) ;
}
std ::path ::Component ::CurDir = > { }
other = > out . push ( other ) ,
}
}
out
}
fn inside ( workdir : & Path , p : & Path ) -> bool {
p . starts_with ( workdir )
}
/// Files a bot never reads or writes: secrets and version-control internals
/// (the original blocks reads/writes by ignore files and repo-block globs),
/// plus this app's own data folder, where API keys are saved.
pub fn is_ignored ( p : & Path ) -> bool {
if let Some ( d ) = crate ::model ::State ::path ( ) . parent ( ) {
if p . starts_with ( d ) {
return true ;
}
}
let s = p . to_string_lossy ( ) . replace ( '\\' , " / " ) ;
let name = p . file_name ( ) . map ( | n | n . to_string_lossy ( ) . to_lowercase ( ) ) . unwrap_or_default ( ) ;
s . contains ( " /.git/ " ) | | s . ends_with ( " /.git " ) | | s . contains ( " /.ssh/ " ) | | s . contains ( " /.gnupg/ " ) | | s . contains ( " /Library/Keychains " )
| | name = = " .env " | | name . starts_with ( " .env. " ) | | name . ends_with ( " .pem " ) | | name . ends_with ( " .key " ) | | name . starts_with ( " id_rsa " ) | | name . starts_with ( " id_ed25519 " )
| | name = = " .netrc " | | name = = " credentials "
}
// ---------- permissions ----------
#[ derive(Debug, PartialEq, Clone) ]
pub enum Decision {
Allow ,
Ask { reason : String , grant : String } ,
Deny ( String ) ,
}
/// Commands that only look at things; chains made only of these run without asking.
const READ_ONLY_COMMANDS : [ & str ; 24 ] = [
" ls " , " pwd " , " cat " , " head " , " tail " , " wc " , " echo " , " rg " , " grep " , " find " , " file " , " stat " , " du " , " df " , " date " , " which " , " tree " , " sort " , " uniq " , " diff " ,
" git status " , " git diff " , " git log " , " git show " ,
] ;
/// Always refused.
const DENIED_COMMANDS : [ & str ; 10 ] = [ " sudo " , " su " , " rm -rf / " , " rm -rf ~ " , " mkfs " , " dd " , " shutdown " , " reboot " , " diskutil erase " , " chmod -R 777 / " ] ;
/// Split `a && b | c; d` into its commands, keeping quoted text together.
/// Returns None when the command hides other commands ($(), backticks, eval).
pub fn split_commands ( cmd : & str ) -> Option < Vec < String > > {
let mut parts = vec! [ ] ;
let mut cur = String ::new ( ) ;
let ( mut sq , mut dq ) = ( false , false ) ;
let chars : Vec < char > = cmd . chars ( ) . collect ( ) ;
let mut i = 0 ;
while i < chars . len ( ) {
let c = chars [ i ] ;
let next = chars . get ( i + 1 ) . copied ( ) ;
if c = = '\'' & & ! dq {
sq = ! sq ;
} else if c = = '"' & & ! sq {
dq = ! dq ;
} else if ! sq & & ( c = = '`' | | ( c = = '$' & & next = = Some ( '(' ) ) ) {
return None ;
} else if ! sq & & ! dq & & ( c = = ';' | | c = = '\n' | | c = = '|' | | c = = '&' ) {
if ! cur . trim ( ) . is_empty ( ) {
parts . push ( cur . trim ( ) . to_string ( ) ) ;
}
cur . clear ( ) ;
if ( c = = '|' | | c = = '&' ) & & next = = Some ( c ) {
i + = 1 ;
}
i + = 1 ;
continue ;
}
cur . push ( c ) ;
i + = 1 ;
}
if sq | | dq {
return None ;
}
if ! cur . trim ( ) . is_empty ( ) {
parts . push ( cur . trim ( ) . to_string ( ) ) ;
}
if parts . iter ( ) . any ( | p | { let w = p . split_whitespace ( ) . next ( ) . unwrap_or ( " " ) ; w = = " eval " | | w = = " exec " | | w = = " source " | | w = = " . " } ) {
return None ;
}
Some ( parts )
}
fn words ( cmd : & str ) -> Vec < & str > {
cmd . split_whitespace ( ) . collect ( )
}
fn starts_with_words ( cmd : & str , prefix : & str ) -> bool {
let ( c , p ) = ( words ( cmd ) , words ( prefix ) ) ;
! p . is_empty ( ) & & c . len ( ) > = p . len ( ) & & c [ .. p . len ( ) ] = = p [ .. ]
}
2026-11-05 07:48:20 +00:00
/// The denied command hiding inside a wrapper: `(sudo …)`, `FOO=1 sudo …`, `env sudo …`,
/// `nohup sudo …` all run the same thing, so peel the wrappings off before matching.
fn denied_hit ( part : & str ) -> Option < & 'static str > {
let mut toks : Vec < String > = Vec ::new ( ) ;
for w in words ( part ) {
let mut w = w ;
while matches! ( w . as_bytes ( ) . first ( ) , Some ( b '(' ) | Some ( b '{' ) ) {
w = & w [ 1 .. ] ;
}
if w . is_empty ( ) {
continue ;
}
// VAR=val prefixes set the environment; they are not the command.
let is_assign = w . split_once ( '=' ) . map ( | ( k , v ) | ! k . is_empty ( ) & & v . len ( ) + 1 < w . len ( ) & & k . chars ( ) . all ( | c | c . is_ascii_alphanumeric ( ) | | c = = '_' ) ) . unwrap_or ( false ) ;
if toks . is_empty ( ) & & is_assign {
continue ;
}
toks . push ( w . to_string ( ) ) ;
}
const WRAP : [ & str ; 7 ] = [ " env " , " command " , " nohup " , " time " , " nice " , " stdbuf " , " timeout " ] ;
while toks . first ( ) . map ( | t | WRAP . contains ( & t . as_str ( ) ) ) . unwrap_or ( false ) {
toks . remove ( 0 ) ;
while toks . first ( ) . map ( | t | t . starts_with ( '-' ) ) . unwrap_or ( false ) {
toks . remove ( 0 ) ;
}
// timeout takes a duration before the command.
if toks . first ( ) . map ( | t | ! t . is_empty ( ) & & t . chars ( ) . next ( ) . map ( | c | c . is_ascii_digit ( ) ) . unwrap_or ( false ) ) . unwrap_or ( false ) {
toks . remove ( 0 ) ;
}
}
let joined = toks . join ( " " ) ;
DENIED_COMMANDS . iter ( ) . find ( | d | starts_with_words ( & joined , d ) ) . copied ( )
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
/// The "always allow" grant for a command: its first word, or first two for git/npm/cargo-style tools.
pub fn shell_grant ( part : & str ) -> String {
let w = words ( part ) ;
let two = [ " git " , " npm " , " pnpm " , " yarn " , " cargo " , " brew " , " pip " , " pip3 " , " python3 " , " docker " , " go " ] ;
let key = if w . len ( ) > = 2 & & two . contains ( & w [ 0 ] ) { format! ( " {} {} " , w [ 0 ] , w [ 1 ] ) } else { w . first ( ) . map ( | s | s . to_string ( ) ) . unwrap_or_default ( ) } ;
format! ( " shell: {key} " )
}
fn writes_files ( part : & str ) -> bool {
part . contains ( '>' )
}
pub fn decide ( name : & str , args : & str , workdir : & Path , grants : & [ String ] ) -> Decision {
let v = args_of ( args ) ;
let granted = | g : & str | grants . iter ( ) . any ( | x | x = = g ) ;
match name {
" list_dir " | " search " | " read_file " = > {
let p = resolve ( workdir , & arg_str ( & v , " path " ) ) ;
if is_ignored ( & p ) {
return Decision ::Deny ( format! ( " 「 {} 」是私密或系統檔案, Bot 不能讀取。 " , p . display ( ) ) ) ;
}
if inside ( workdir , & p ) | | granted ( " read:anywhere " ) {
Decision ::Allow
} else {
Decision ::Ask { reason : format ! ( " 讀取工作資料夾以外的地方:{} " , p . display ( ) ) , grant : " read:anywhere " . into ( ) }
}
}
" read_terminal " | " write_terminal " = > Decision ::Allow ,
" edit_file " = > {
let p = resolve ( workdir , & arg_str ( & v , " path " ) ) ;
if is_ignored ( & p ) {
return Decision ::Deny ( format! ( " 「 {} 」是私密或系統檔案, Bot 不能修改。 " , p . display ( ) ) ) ;
}
if ! inside ( workdir , & p ) {
return Decision ::Ask { reason : format ! ( " 修改工作資料夾以外的檔案:{} " , p . display ( ) ) , grant : String ::new ( ) } ;
}
if granted ( " edit_file " ) {
Decision ::Allow
} else {
Decision ::Ask { reason : format ! ( " 修改檔案:{} " , p . display ( ) ) , grant : " edit_file " . into ( ) }
}
}
" shell " = > {
let cmd = arg_str ( & v , " command " ) ;
if cmd . trim ( ) . is_empty ( ) {
return Decision ::Deny ( " 沒有指令 " . into ( ) ) ;
}
let Some ( parts ) = split_commands ( & cmd ) else {
2026-11-05 13:28:32 +00:00
// Commands the splitter can't read ($(), backticks, eval…) ask once and remember
// by their first word: 「一律允許」 on `git` covers `git … $(…)` afterwards, so
// the same shape of command doesn't ask again (models use $(…) constantly).
if let Some ( d ) = denied_hit ( & cmd ) {
return Decision ::Deny ( format! ( " 「 {d} 」這類指令一律不允許。 " ) ) ;
}
let grant = shell_grant ( & cmd ) ;
if ! writes_files ( & cmd ) & & granted ( & grant ) {
return Decision ::Allow ;
}
return Decision ::Ask { reason : format ! ( " 執行指令(含有隱藏的子指令,無法自動判斷):{cmd} " ) , grant } ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
} ;
// deny
for part in & parts {
2026-11-05 07:48:20 +00:00
if let Some ( d ) = denied_hit ( part ) {
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
return Decision ::Deny ( format! ( " 「 {d} 」這類指令一律不允許。 " ) ) ;
}
}
2026-11-05 13:28:32 +00:00
// (No word-by-word path scan here: secret folders are the sandbox's job, and
// scanning words as paths denied perfectly ordinary commands.)
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
// allow: every part is read-only or already granted
let ok = | part : & String | ! writes_files ( part ) & & ( READ_ONLY_COMMANDS . iter ( ) . any ( | r | starts_with_words ( part , r ) ) | | granted ( & shell_grant ( part ) ) ) ;
let unapproved : Vec < & String > = parts . iter ( ) . filter ( | p | ! ok ( p ) ) . collect ( ) ;
if unapproved . is_empty ( ) {
return Decision ::Allow ;
}
let grant = if unapproved . len ( ) = = 1 & & ! writes_files ( unapproved [ 0 ] ) { shell_grant ( unapproved [ 0 ] ) } else { String ::new ( ) } ;
Decision ::Ask { reason : format ! ( " 執行指令:{cmd} " ) , grant }
}
other = > Decision ::Deny ( format! ( " 沒有「 {other} 」這個工具 " ) ) ,
}
}
// ---------- process trees ----------
// From cursor_proclist.node (the original's native process scanner):
// getDescendantPids @0x6748: breadth-first over children by parent pid (proc_listpids PROC_PPID_ONLY),
// de-duplicated, stops once 0xfff (4095) pids are collected → at most 4096 processes.
// ProclistScanWorker::Execute @0x3d00: scans the roots plus all descendants.
// getProcessInformation @0x5234: memoryMB = resident bytes >> 20 (lsr #0x14);
// cpuTimeMs = (user + system time, in Mach ticks × timebase) / 1 000 000;
// each process is tied to its agent by CONVERSATION_ID= / REQUEST_ID= in its environment.
pub const MAX_TRACKED_PROCESSES : usize = 4096 ;
/// Our own names for the tags the original reads (it uses its vendor prefix).
pub const ENV_CONVERSATION_ID : & str = " ANYBOT_CONVERSATION_ID " ;
pub const ENV_REQUEST_ID : & str = " ANYBOT_REQUEST_ID " ;
#[ derive(Clone, Copy, Debug) ]
pub struct ProcRow {
pub pid : u32 ,
pub ppid : u32 ,
pub rss_kb : u64 ,
pub cpu_ms : u64 ,
}
/// `[[dd-]hh:]mm:ss[.cc]` as printed by ps, in milliseconds.
pub fn parse_cpu_time ( s : & str ) -> u64 {
let ( days , rest ) = match s . split_once ( '-' ) {
Some ( ( d , r ) ) = > ( d . parse ::< u64 > ( ) . unwrap_or ( 0 ) , r ) ,
None = > ( 0 , s ) ,
} ;
let mut secs = 0 f64 ;
for part in rest . split ( ':' ) {
secs = secs * 60.0 + part . parse ::< f64 > ( ) . unwrap_or ( 0.0 ) ;
}
days * 86_400_000 + ( secs * 1000.0 ) . round ( ) as u64
}
pub fn proc_table ( ) -> Vec < ProcRow > {
let Ok ( out ) = Command ::new ( " ps " ) . args ( [ " -A " , " -o " , " pid=,ppid=,rss=,time= " ] ) . output ( ) else { return vec! [ ] } ;
String ::from_utf8_lossy ( & out . stdout )
. lines ( )
. filter_map ( | l | {
let f : Vec < & str > = l . split_whitespace ( ) . collect ( ) ;
( f . len ( ) > = 4 ) . then ( | | ProcRow { pid : f [ 0 ] . parse ( ) . unwrap_or ( 0 ) , ppid : f [ 1 ] . parse ( ) . unwrap_or ( 0 ) , rss_kb : f [ 2 ] . parse ( ) . unwrap_or ( 0 ) , cpu_ms : parse_cpu_time ( f [ 3 ] ) } )
} )
. filter ( | r | r . pid > 0 )
. collect ( )
}
/// The roots and everything they started, breadth-first, without repeats, at most 4096.
pub fn descendants ( table : & [ ProcRow ] , roots : & [ u32 ] ) -> Vec < u32 > {
let mut seen = std ::collections ::HashSet ::new ( ) ;
let mut out = vec! [ ] ;
let mut queue : std ::collections ::VecDeque < u32 > = roots . iter ( ) . copied ( ) . filter ( | p | * p > 0 ) . collect ( ) ;
while let Some ( p ) = queue . pop_front ( ) {
if ! seen . insert ( p ) {
continue ;
}
out . push ( p ) ;
if out . len ( ) > = MAX_TRACKED_PROCESSES {
break ;
}
queue . extend ( table . iter ( ) . filter ( | r | r . ppid = = p ) . map ( | r | r . pid ) ) ;
}
out
}
/// (processes, cpu ms, memory MB) for a command and everything it started.
pub fn tree_stats ( root : u32 ) -> ( usize , u64 , u64 ) {
let table = proc_table ( ) ;
let pids = descendants ( & table , & [ root ] ) ;
let rows : Vec < & ProcRow > = table . iter ( ) . filter ( | r | pids . contains ( & r . pid ) ) . collect ( ) ;
( rows . len ( ) , rows . iter ( ) . map ( | r | r . cpu_ms ) . sum ( ) , rows . iter ( ) . map ( | r | ( r . rss_kb * 1024 ) > > 20 ) . sum ( ) )
}
/// Stop a command and everything it started (newest first), not just the shell.
pub fn kill_tree ( root : u32 ) {
let table = proc_table ( ) ;
let mut pids = descendants ( & table , & [ root ] ) ;
pids . reverse ( ) ;
if pids . is_empty ( ) {
return ;
}
let _ = Command ::new ( " kill " ) . arg ( " -TERM " ) . args ( pids . iter ( ) . map ( | p | p . to_string ( ) ) ) . stderr ( Stdio ::null ( ) ) . status ( ) ;
}
// ---------- sandbox ----------
// From sand-op-launcher `_main`: before exec it applies a sandbox profile
// "(version 1)(allow default)(deny file-read* (with no-report) (subpath (param …))))" that hides
// one private folder, builds a fresh environment, and refuses invocations outside an exact list.
// Here: commands can't read or write the app's own data (where API keys are saved) or the
// usual secret folders, and API keys are removed from their environment.
const KEY_ENV_VARS : [ & str ; 6 ] = [ " XAI_API_KEY " , " OPENAI_API_KEY " , " ANTHROPIC_API_KEY " , " GEMINI_API_KEY " , " OPENROUTER_API_KEY " , " ANTHROPIC_AUTH_TOKEN " ] ;
pub fn protected_dirs ( ) -> Vec < PathBuf > {
let mut v = vec! [ ] ;
if let Some ( d ) = crate ::model ::State ::path ( ) . parent ( ) {
v . push ( d . to_path_buf ( ) ) ;
}
if let Some ( h ) = dirs ::home_dir ( ) {
for s in [ " .ssh " , " .gnupg " , " Library/Keychains " , " .aws " , " .config/gcloud " ] {
v . push ( h . join ( s ) ) ;
}
}
v
}
2026-11-04 13:52:06 +00:00
/// Inside the protected folders, what commands may still read: the chat's attachments (a picture
2026-11-05 13:28:32 +00:00
/// the person sent is the bot's to look at; the keys next to it are not) and the ssh keys, so
/// git and ssh can reach remote repos (writing them stays denied).
2026-11-04 13:52:06 +00:00
pub fn readable_dirs ( ) -> Vec < PathBuf > {
2026-11-05 13:28:32 +00:00
let mut v : Vec < PathBuf > = crate ::model ::State ::path ( ) . parent ( ) . map ( | d | vec! [ d . join ( " attachments " ) ] ) . unwrap_or_default ( ) ;
if let Some ( h ) = dirs ::home_dir ( ) {
v . push ( h . join ( " .ssh " ) ) ;
}
v
2026-11-04 13:52:06 +00:00
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
fn sandbox_exec ( ) -> Option < & 'static str > {
let p = " /usr/bin/sandbox-exec " ;
( cfg! ( target_os = " macos " ) & & Path ::new ( p ) . exists ( ) ) . then_some ( p )
}
Whole control flow checked against real chats: what the bot sees is what is there, every step lands, nothing done twice
Web
- web_fetch kept pages with a <header> only up to it (taken for <head>): articles came back as their menus.
- browser_open waits for the page to load and returns it like browser_read (one step fewer per page);
actions wait for the page to hold still instead of fixed 1-2.5 s sleeps; one way to reach the tab.
The bot's computer (box v6)
- Numbers only on what can be seen: clipped to the window and the page's visible area, none under
another app's window (a link scrolled under the toolbar was numbered on Back).
- press_key with a typed character no longer swallows the keys after it (xdotool type --args 1).
- The person's input through noVNC/TigerVNC is noticed again (fence, continuous updates, QEMU keys).
- "Nothing changed" only after clicks, scrolls and drags; long text is pasted; one screen step at a
time; one docker inspect per step when the box is known ready; poppler-utils for long PDFs.
- One typing path and fewer round trips (stdin variant, unused picture path, extra batch execs gone).
The person's computers
- Linux: the window system's front app when it has one, and the reason when there is none (was 「」);
hidden and off-screen controls get no number; wording no longer names a tool that doesn't exist.
The loop
- Without Docker, the bot's own computer runs no commands where there is no sandbox (Linux, Windows):
it was a plain shell on the brain itself, keys in reach, asked nothing.
- The bot's own computer is re-checked each step; unreachable computers are failures, not safety
blocks; no direct-mode note on every result; an empty reply is asked again; scrolling and keys
aren't counted as going in circles; steps aren't moved to a computer that can't do them.
- One finish() for every step's result, keep_grant(), dead appctl list_computers/SERVING and the
impossible server branches removed; duplicated prompt instructions merged.
Tests: 162 (Mac) / 166 (Linux) unit; real box drills 6/6 on box v6; a brain without Chrome browses
on its box; remote E2E 28/28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 09:44:59 +00:00
/// Can the bot's own computer run commands without Docker? Only inside a sandbox (macOS):
/// elsewhere a command there would run on this computer itself, as AnyBot, keys and all.
pub fn sealed_runs_commands ( ) -> bool {
sandbox_exec ( ) . is_some ( )
}
/// Why the bot's own computer can't run a command right now.
pub const NO_BOX_FOR_COMMANDS : & str = " 你自己的電腦現在不能執行指令: AnyBot 所在的這台沒有 Docker( 或連不到 Docker) , 沒有隔離的地方可以跑指令。請告訴使用者把 Docker 打開,或請他指名別台電腦來做。 " ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
fn real ( p : & Path ) -> String {
// The sandbox matches real paths, so resolve links like /var → /private/var.
std ::fs ::canonicalize ( p ) . unwrap_or_else ( | _ | p . to_path_buf ( ) ) . display ( ) . to_string ( )
}
/// The shell command, wrapped in the sandbox when the system has one.
#[ cfg(test) ]
pub fn build_command ( cmd : & str , workdir : & Path , protected : & [ PathBuf ] , conversation : & str , request : & str ) -> Command {
2026-11-04 13:52:06 +00:00
build_command_sealed ( cmd , workdir , protected , & [ ] , None , conversation , request )
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
Bots work like someone who knows the computer: they know each machine, reach what they see, plan, and hand the planned steps to a quick model
Measured on the bot's own box (8 tasks x 2, grok-4.7): 14/16 done, 120 steps, 1114 s before;
16/16, 102 steps, 574 s after (model time 1015 -> 471 s). A replayed skill: 49 s -> 15 s.
Knowing the machine
- facts.rs: 電腦現況 per computer (system, desktop, systemd, sudo, network, package manager,
tools, apps, a fixed TZ), read once by a small read-only probe, kept 30 min, in the
instructions at the start of a turn.
- The box's sudo is the bot's: only what follows sudo is checked against the always-denied
list (it was refused outright, while the instructions said it could sudo), and commands with
sudo run as the box's root (setuid does nothing under gVisor).
- Box image v8 drops ENV TZ=UTC (it overrode /etc/localtime). ANYBOT_BOX_IMAGE /
ANYBOT_BOX_VOLUME give a throwaway box its own image and volume, never adopting the real one.
- Mac shell sandbox: App Store app CLIs (they carry their own app sandbox) and ps/top run
outside it; plain `osascript -e` too, asked the first time even in direct mode, refusing do
shell script, terminals, keystrokes, file reads, ObjC and the secret folders. A command the
sandbox broke says so, instead of looking like a broken program.
Reaching what it sees
- The Mac menu bar's status icons are numbered (each app's AXExtrasMenuBar, named from inside
for the system's own), computer_use_zoom looks closer at full sharpness.
- Numbers stay with their controls from look to look (they were reassigned every look, so a
step's second click landed elsewhere); the box's front window is listed first; a control can
be named instead of numbered (name "4").
- Web page numbering walks open shadow DOM (Chromium settings, web components).
- A box on a Docker host elsewhere retries the next port when the usual one is taken (a retry
without it came up with no network).
Working like a person
- Instructions: pick the route, know what done looks like, one route, check once, say what's
stuck instead of faking it with a substitute, ask before cutting the connection.
- StepPlan: the bot's model takes the first step, the step after trouble (failed, refused,
repeated, nothing changed on screen, the quick model switching routes), one in four, and the
final check; a quick model (Bot.step_model, auto: grok-4.20-0309-non-reasoning on xAI) does
the planned steps in between, falling back to the bot's model if it can't be used.
- Skills keep the steps that worked (with what each acted on, not numbers); replay_skill does
them again with no model in between, finding each control again, handing back on a mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
2026-11-09 01:01:03 +00:00
/// Characters that join, redirect or substitute commands: a command with any of them is not one
/// plain program call.
const SHELL_JOINERS : [ & str ; 9 ] = [ " ; " , " & " , " | " , " < " , " > " , " ` " , " $( " , " \n " , " \\ " ] ;
/// The program a plain one-program command runs, quotes stripped ("" when it isn't one).
fn plain_program ( cmd : & str ) -> Option < String > {
let c = cmd . trim ( ) ;
if c . is_empty ( ) | | SHELL_JOINERS . iter ( ) . any ( | j | c . contains ( j ) ) {
return None ;
}
let first = if let Some ( rest ) = c . strip_prefix ( '"' ) {
rest . split ( '"' ) . next ( ) ? . to_string ( )
} else if let Some ( rest ) = c . strip_prefix ( '\'' ) {
rest . split ( '\'' ) . next ( ) ? . to_string ( )
} else {
c . split_whitespace ( ) . next ( ) ? . to_string ( )
} ;
Some ( first )
}
/// An App Store app's own command line (`/Applications/X.app/Contents/MacOS/Y`): it carries
/// Apple's app sandbox, so outside AnyBot's sandbox it still can't read the secret folders —
/// and inside it, the two sandboxes clash and the program dies at once (Tailscale's CLI:
/// "Trace/BPT trap: 5"). Checked once per program with codesign.
fn app_sandboxed_cli ( program : & str ) -> bool {
let p = Path ::new ( program ) ;
let in_app = p . is_absolute ( )
& & ! program . contains ( " .. " )
& & p . components ( ) . count ( ) > = 6
& & program . starts_with ( " /Applications/ " )
& & program . split ( '/' ) . nth ( 2 ) . map ( | a | a . ends_with ( " .app " ) ) . unwrap_or ( false )
& & program . contains ( " .app/Contents/MacOS/ " ) ;
if ! in_app | | ! p . is_file ( ) {
return false ;
}
static SEEN : OnceLock < Mutex < HashMap < String , bool > > > = OnceLock ::new ( ) ;
let seen = SEEN . get_or_init ( | | Mutex ::new ( HashMap ::new ( ) ) ) ;
if let Some ( v ) = seen . lock ( ) . unwrap_or_else ( | e | e . into_inner ( ) ) . get ( program ) {
return * v ;
}
let ok = Command ::new ( " /usr/bin/codesign " )
. args ( [ " -d " , " --entitlements " , " - " , " --xml " , program ] )
. stderr ( Stdio ::null ( ) )
. output ( )
. map ( | o | {
let x = String ::from_utf8_lossy ( & o . stdout ) . replace ( [ '\n' , '\t' , ' ' ] , " " ) ;
x . contains ( " <key>com.apple.security.app-sandbox</key><true/> " )
} )
. unwrap_or ( false ) ;
seen . lock ( ) . unwrap_or_else ( | e | e . into_inner ( ) ) . insert ( program . to_string ( ) , ok ) ;
ok
}
/// `ps` / `top`, alone or piped into plain text filters that take no file: they need system
/// rights (setuid), which no sandboxed process gets ("/bin/ps: Operation not permitted"), and
/// they read no files.
fn process_listing ( cmd : & str ) -> bool {
let c = cmd . trim ( ) ;
if [ " ; " , " & " , " < " , " > " , " ` " , " $( " , " \n " , " \\ " , " ~ " ] . iter ( ) . any ( | j | c . contains ( j ) ) {
return false ;
}
let mut parts = c . split ( '|' ) . map ( str ::trim ) ;
let Some ( first ) = parts . next ( ) else { return false } ;
let mut w = first . split_whitespace ( ) ;
let prog = w . next ( ) . unwrap_or ( " " ) ;
if ! matches! ( prog , " ps " | " /bin/ps " | " top " | " /usr/bin/top " ) | | w . any ( | a | a . contains ( '/' ) ) {
return false ;
}
parts . all ( | p | {
let mut w = p . split_whitespace ( ) ;
matches! ( w . next ( ) . unwrap_or ( " " ) , " grep " | " egrep " | " head " | " tail " | " wc " | " sort " | " uniq " | " cut " | " awk " ) & & ! p . contains ( '/' )
} )
}
/// A command's words the way the shell reads them, or None when it is more than one plain
/// program call: anything outside quotes that joins, redirects, pipes, backgrounds or
/// substitutes (`;` `&` `|` `<` `>` `(` `)` a backtick, `$(`, a line break), and substitution
/// inside double quotes. Quotes, backslashes and `&` inside an AppleScript string are fine.
pub fn plain_words ( cmd : & str ) -> Option < Vec < String > > {
let mut words = vec! [ ] ;
let mut cur = String ::new ( ) ;
let mut started = false ;
let mut it = cmd . trim ( ) . chars ( ) . peekable ( ) ;
while let Some ( c ) = it . next ( ) {
match c {
'\'' = > {
started = true ;
loop {
match it . next ( ) ? {
'\'' = > break ,
ch = > cur . push ( ch ) ,
}
}
}
'"' = > {
started = true ;
loop {
match it . next ( ) ? {
'"' = > break ,
'\\' = > match it . next ( ) ? {
e @ ( '"' | '\\' | '$' | '`' ) = > cur . push ( e ) ,
'\n' = > { }
e = > {
cur . push ( '\\' ) ;
cur . push ( e ) ;
}
} ,
'`' = > return None ,
'$' if it . peek ( ) = = Some ( & '(' ) = > return None ,
ch = > cur . push ( ch ) ,
}
}
}
'\\' = > {
started = true ;
match it . next ( ) ? {
'\n' = > { }
e = > cur . push ( e ) ,
}
}
';' | '&' | '|' | '<' | '>' | '(' | ')' | '`' | '\n' = > return None ,
'$' if it . peek ( ) = = Some ( & '(' ) = > return None ,
c if c . is_whitespace ( ) = > {
if started {
words . push ( std ::mem ::take ( & mut cur ) ) ;
started = false ;
}
}
c = > {
started = true ;
cur . push ( c ) ;
}
}
}
if started {
words . push ( cur ) ;
}
( ! words . is_empty ( ) ) . then_some ( words )
}
/// What an AppleScript run outside the sandbox may not do: run shell commands or other
/// scripts, drive a terminal or type keystrokes (into one), read files or reach the Objective-C
/// bridge, or name the secret folders. Outside the sandbox nothing else stops it, so this list
/// errs on the side of refusing; refused scripts still run, inside the sandbox.
const OSASCRIPT_OUTSIDE_REFUSED : [ & str ; 24 ] = [
" do shell script " , " run script " , " load script " , " store script " , " do script " , " terminal " , " iterm " , " warp " , " ghostty " , " keystroke " , " key code " ,
" objc " , " $.ns " , " nsstring " , " nsdata " , " nsfilemanager " , " read file " , " read posix file " , " open for access " , " .ssh " , " keychain " , " .gnupg " , " .aws " , " gcloud " ,
] ;
/// A plain `osascript -e …` call that may run outside the sandbox (Apple events to other apps
/// only work there: inside, every app answers -10004), and why not when it may not.
pub fn osascript_outside ( cmd : & str ) -> Result < ( ) , String > {
let words = plain_words ( cmd ) . ok_or ( " 不是單獨一條 osascript 指令 " ) ? ;
if ! matches! ( words [ 0 ] . as_str ( ) , " osascript " | " /usr/bin/osascript " ) {
return Err ( " 不是 osascript " . into ( ) ) ;
}
// Only inline AppleScript (-e …): no script files (unseen code), no JavaScript (its ObjC
// bridge reads anything), no interactive mode.
let mut script = String ::new ( ) ;
let mut i = 1 ;
while i < words . len ( ) {
match words [ i ] . as_str ( ) {
" -e " if i + 1 < words . len ( ) = > {
script . push_str ( & words [ i + 1 ] ) ;
script . push ( '\n' ) ;
i + = 2 ;
}
" -s " if i + 1 < words . len ( ) = > i + = 2 ,
w if w . starts_with ( " -s " ) = > i + = 1 ,
_ = > return Err ( " 只能用 -e 寫 AppleScript( 不能跑腳本檔、JavaScript 或互動模式) " . into ( ) ) ,
}
}
if script . trim ( ) . is_empty ( ) {
return Err ( " 沒有 -e 的 AppleScript " . into ( ) ) ;
}
let low = script . to_lowercase ( ) ;
let home = dirs ::home_dir ( ) . map ( | h | h . display ( ) . to_string ( ) . to_lowercase ( ) ) . unwrap_or_default ( ) ;
let data = crate ::model ::State ::path ( ) . parent ( ) . map ( | d | d . display ( ) . to_string ( ) . to_lowercase ( ) ) . unwrap_or_default ( ) ;
if let Some ( bad ) = OSASCRIPT_OUTSIDE_REFUSED . iter ( ) . find ( | b | low . contains ( * b ) ) {
return Err ( format! ( " 用到了「 {bad} 」 " ) ) ;
}
if ( ! data . is_empty ( ) & & low . contains ( & data ) ) | | low . contains ( " application support/anybot " ) | | ( ! home . is_empty ( ) & & low . contains ( & format! ( " {home} /library/keychains " ) ) ) {
return Err ( " 碰到 AnyBot 的資料夾或鑰匙圈 " . into ( ) ) ;
}
Ok ( ( ) )
}
/// Commands that run outside AnyBot's sandbox on the person's Mac, because the sandbox breaks
/// them and they can't reach the secret folders anyway (see app_sandboxed_cli, process_listing),
/// or because the person allowed AppleScript to drive their apps (osascript_outside; asked first).
pub fn runs_outside_sandbox ( cmd : & str ) -> bool {
if ! cfg! ( target_os = " macos " ) {
return false ;
}
process_listing ( cmd ) | | osascript_outside ( cmd ) . is_ok ( ) | | plain_program ( cmd ) . map ( | p | app_sandboxed_cli ( & p ) ) . unwrap_or ( false )
}
/// The grant that lets AppleScript run outside the sandbox without asking again.
pub const OSASCRIPT_GRANT : & str = " osascript:outside " ;
/// A word for the bot when a command failed because of the sandbox, not because the program or
/// the computer is broken — so it changes course instead of trying the same thing again.
pub fn sandbox_note ( cmd : & str , out : & str ) -> Option < & 'static str > {
sandbox_exec ( ) ? ;
if runs_outside_sandbox ( cmd ) {
return None ;
}
let setuid = [ " /bin/ps: Operation not permitted " , " /usr/bin/top: Operation not permitted " , " sudo: Operation not permitted " , " /usr/bin/sudo: Operation not permitted " ] ;
if setuid . iter ( ) . any ( | s | out . contains ( s ) ) {
return Some ( " (這是 AnyBot 的指令沙盒擋下的, 不是電腦壞了: ps、top、sudo 這類要系統權限的程式在沙盒裡不能跑。查程序改用 pgrep -fl 名字,或把 ps/ top 單獨執行(後面只接 grep、head 這類過濾),那樣會在沙盒外跑。) " ) ;
}
if out . contains ( " (-10004) " ) | | out . contains ( " 越權取用 " ) | | out . to_lowercase ( ) . contains ( " privilege violation " ) {
return Some ( match osascript_outside ( cmd ) {
// Plain osascript that still got -10004 can't be the sandbox: macOS itself said no.
Ok ( ( ) ) = > return None ,
Err ( _ ) if cmd . contains ( " osascript " ) = > " (這是 AnyBot 的指令沙盒擋下的,不是那個 App 壞了: AppleScript 只有寫成單獨一條 osascript -e '…' 指令(不接其他指令、不用 heredoc, 多行就多個 -e) , 而且不跑 shell 指令、不碰終端機、不讀檔案時,才會在沙盒外跑、能指揮別的 App。改寫成那樣, 或改用畫面操作( computer_use_*)。) " ,
Err ( _ ) = > " (這是 AnyBot 的指令沙盒擋下的,不是那個 App 壞了:在沙盒裡不能指揮別的 App( -10004) 。改用畫面操作( computer_use_*),或寫成單獨一條 osascript -e '…' 指令。) " ,
} ) ;
}
if ( out . contains ( " Trace/BPT trap " ) | | out . contains ( " 指令因為收到訊號而終止 " ) ) & & cmd . contains ( " .app/Contents/MacOS/ " ) {
return Some ( " (這個 App 的指令列在 AnyBot 的沙盒裡會當掉,不是 App 壞了。用它的完整路徑單獨執行(不要接 ;、|、&& 或其他指令) , App Store 版的 App 就會在沙盒外跑;不行就改用畫面操作。) " ) ;
}
None
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
/// `seal = Some(home)`: the bot's own computer. Nothing under the person's home folder is
/// visible or writable except the bot's folder (`workdir`), like a separate machine.
2026-11-04 13:52:06 +00:00
/// `readable`: folders inside `protected` that may still be read (never written).
pub fn build_command_sealed ( cmd : & str , workdir : & Path , protected : & [ PathBuf ] , readable : & [ PathBuf ] , seal : Option < & Path > , conversation : & str , request : & str ) -> Command {
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let mut c = match sandbox_exec ( ) {
Bots work like someone who knows the computer: they know each machine, reach what they see, plan, and hand the planned steps to a quick model
Measured on the bot's own box (8 tasks x 2, grok-4.7): 14/16 done, 120 steps, 1114 s before;
16/16, 102 steps, 574 s after (model time 1015 -> 471 s). A replayed skill: 49 s -> 15 s.
Knowing the machine
- facts.rs: 電腦現況 per computer (system, desktop, systemd, sudo, network, package manager,
tools, apps, a fixed TZ), read once by a small read-only probe, kept 30 min, in the
instructions at the start of a turn.
- The box's sudo is the bot's: only what follows sudo is checked against the always-denied
list (it was refused outright, while the instructions said it could sudo), and commands with
sudo run as the box's root (setuid does nothing under gVisor).
- Box image v8 drops ENV TZ=UTC (it overrode /etc/localtime). ANYBOT_BOX_IMAGE /
ANYBOT_BOX_VOLUME give a throwaway box its own image and volume, never adopting the real one.
- Mac shell sandbox: App Store app CLIs (they carry their own app sandbox) and ps/top run
outside it; plain `osascript -e` too, asked the first time even in direct mode, refusing do
shell script, terminals, keystrokes, file reads, ObjC and the secret folders. A command the
sandbox broke says so, instead of looking like a broken program.
Reaching what it sees
- The Mac menu bar's status icons are numbered (each app's AXExtrasMenuBar, named from inside
for the system's own), computer_use_zoom looks closer at full sharpness.
- Numbers stay with their controls from look to look (they were reassigned every look, so a
step's second click landed elsewhere); the box's front window is listed first; a control can
be named instead of numbered (name "4").
- Web page numbering walks open shadow DOM (Chromium settings, web components).
- A box on a Docker host elsewhere retries the next port when the usual one is taken (a retry
without it came up with no network).
Working like a person
- Instructions: pick the route, know what done looks like, one route, check once, say what's
stuck instead of faking it with a substitute, ask before cutting the connection.
- StepPlan: the bot's model takes the first step, the step after trouble (failed, refused,
repeated, nothing changed on screen, the quick model switching routes), one in four, and the
final check; a quick model (Bot.step_model, auto: grok-4.20-0309-non-reasoning on xAI) does
the planned steps in between, falling back to the bot's model if it can't be used.
- Skills keep the steps that worked (with what each acted on, not numbers); replay_skill does
them again with no model in between, finding each control again, handing back on a mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
2026-11-09 01:01:03 +00:00
Some ( se ) if ( ! protected . is_empty ( ) | | seal . is_some ( ) ) & & ! ( seal . is_none ( ) & & runs_outside_sandbox ( cmd ) ) = > {
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let rules : String = ( 0 .. protected . len ( ) ) . map ( | i | format! ( " (subpath (param \" P {i} \" )) " ) ) . collect ( ) ;
let mut profile = format! ( " (version 1)(allow default)(deny file-read* file-write* (with no-report) {rules} ) " ) ;
if seal . is_some ( ) {
profile . push_str ( " (deny file-read* file-write* (with no-report) (require-all (subpath (param \" HOME \" )) (require-not (subpath (param \" ROOT \" ))))) " ) ;
}
2026-11-04 13:52:06 +00:00
// Later rules win: reading these is allowed again (writing stays denied).
let open : Vec < & PathBuf > = readable . iter ( ) . filter ( | r | protected . iter ( ) . any ( | p | r . starts_with ( p ) ) ) . collect ( ) ;
for i in 0 .. open . len ( ) {
profile . push_str ( & format! ( " (allow file-read* (subpath (param \" R {i} \" ))) " ) ) ;
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let mut c = Command ::new ( se ) ;
for ( i , p ) in protected . iter ( ) . enumerate ( ) {
c . arg ( " -D " ) . arg ( format! ( " P {i} = {} " , real ( p ) ) ) ;
}
2026-11-04 13:52:06 +00:00
for ( i , r ) in open . iter ( ) . enumerate ( ) {
c . arg ( " -D " ) . arg ( format! ( " R {i} = {} " , real ( r ) ) ) ;
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
if let Some ( home ) = seal {
c . arg ( " -D " ) . arg ( format! ( " HOME= {} " , real ( home ) ) ) . arg ( " -D " ) . arg ( format! ( " ROOT= {} " , real ( workdir ) ) ) ;
}
c . arg ( " -p " ) . arg ( profile ) . arg ( " /bin/sh " ) . arg ( " -c " ) . arg ( cmd ) ;
c
}
_ = > {
let mut c = Command ::new ( " /bin/sh " ) ;
c . arg ( " -c " ) . arg ( cmd ) ;
c
}
} ;
for k in KEY_ENV_VARS {
c . env_remove ( k ) ;
}
2026-11-05 13:28:32 +00:00
// The app started from Finder or launchd has no shell PATH (no /opt/homebrew/bin): a
// command run on an always-on Mac (a Mac mini under LaunchAgent) must find its tools too.
c . env ( " PATH " , crate ::cloudbox ::tool_path ( ) ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
c . env ( ENV_CONVERSATION_ID , conversation ) . env ( ENV_REQUEST_ID , request ) ;
#[ cfg(unix) ]
{
use std ::os ::unix ::process ::CommandExt ;
c . process_group ( 0 ) ;
}
c . current_dir ( workdir ) . stdin ( Stdio ::piped ( ) ) . stdout ( Stdio ::piped ( ) ) . stderr ( Stdio ::piped ( ) ) ;
c
}
/// A command AnyBot itself wraps around the bot's (ssh / docker exec): no sandbox, because the
/// wrapper needs the person's SSH keys and Docker settings; the bot's part runs on the other machine.
pub fn build_wrapper_command ( cmd : & str , conversation : & str , request : & str ) -> Command {
let mut c = Command ::new ( " /bin/sh " ) ;
c . arg ( " -c " ) . arg ( cmd ) ;
for k in KEY_ENV_VARS {
c . env_remove ( k ) ;
}
c . env ( ENV_CONVERSATION_ID , conversation ) . env ( ENV_REQUEST_ID , request ) . env ( " PATH " , crate ::cloudbox ::tool_path ( ) ) ;
#[ cfg(unix) ]
{
use std ::os ::unix ::process ::CommandExt ;
c . process_group ( 0 ) ;
}
c . current_dir ( std ::env ::temp_dir ( ) ) . stdin ( Stdio ::piped ( ) ) . stdout ( Stdio ::piped ( ) ) . stderr ( Stdio ::piped ( ) ) ;
c
}
// ---------- running tools ----------
/// Which chat and which tool call a command belongs to.
#[ derive(Clone, Default) ]
pub struct RunIds {
pub conversation : String ,
pub request : String ,
}
struct Terminal {
command : String ,
stdin : Arc < Mutex < Option < std ::process ::ChildStdin > > > ,
output : Arc < Mutex < String > > ,
child : Arc < Mutex < std ::process ::Child > > ,
exit : Arc < Mutex < Option < i32 > > > ,
root : u32 ,
/// The bot it belongs to (the ANYBOT_CONVERSATION_ID tag).
conversation : String ,
}
fn terminals ( ) -> & 'static Mutex < HashMap < u64 , Terminal > > {
static T : OnceLock < Mutex < HashMap < u64 , Terminal > > > = OnceLock ::new ( ) ;
T . get_or_init ( | | Mutex ::new ( HashMap ::new ( ) ) )
}
fn next_terminal_id ( ) -> u64 {
static N : std ::sync ::atomic ::AtomicU64 = std ::sync ::atomic ::AtomicU64 ::new ( 1 ) ;
N . fetch_add ( 1 , Ordering ::Relaxed )
}
/// Keep the start and the end of long output (the original's output_head / output_tail).
pub fn head_tail ( s : & str ) -> String {
let n = s . chars ( ) . count ( ) ;
if n < = OUTPUT_HEAD_CHARS + OUTPUT_TAIL_CHARS {
return s . to_string ( ) ;
}
let head : String = s . chars ( ) . take ( OUTPUT_HEAD_CHARS ) . collect ( ) ;
let tail : String = s . chars ( ) . skip ( n - OUTPUT_TAIL_CHARS ) . collect ( ) ;
format! ( " {head} \n …(中間省略 {} 個字)… \n {tail} " , n - OUTPUT_HEAD_CHARS - OUTPUT_TAIL_CHARS )
}
fn timeout_of ( v : & Value ) -> u64 {
v . get ( " timeout_ms " ) . and_then ( Value ::as_u64 ) . unwrap_or ( SHELL_DEFAULT_TIMEOUT_MS ) . min ( SHELL_MAX_TIMEOUT_MS )
}
// ---------- computers ----------
// The original runs a bot's work on "Bot 的電腦" (its own cloud box) and only runs on this Mac when
// the person turns on 在這台電腦上執行 (setting `local-execution`, ids fuUvu2 / Hd1KB+). Permission
// questions name the machine ("允許 Grok Bot 在 {targetMachineDisplay} 上使用 {app} 嗎?", eQViQE).
// Here the bot's own computer is a sealed folder, this Mac is the person's computer, and other
// computers are reached over SSH.
#[ derive(Clone, Debug) ]
pub enum Place {
/// The bot's own computer when Docker isn't there: everything outside `root` in the home
/// folder is invisible.
Sealed { root : PathBuf } ,
/// The bot's own computer: AnyBot's Linux box (Docker), starting in `dir` inside it.
Box { dir : String } ,
/// The person's computer, starting in `workdir`.
Local { workdir : PathBuf } ,
/// Another computer: `ssh target`, starting in `dir` (empty = its home).
Ssh { target : String , dir : String } ,
2026-11-05 13:28:32 +00:00
/// 裝置連結: another AnyBot of the same account (through the relay), by its host id,
/// starting in `dir` (empty = its home).
Device { host : String , dir : String } ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
#[ derive(Clone, Debug) ]
pub struct Target {
pub name : String ,
pub place : Place ,
}
2026-11-05 08:47:28 +00:00
/// Which of a bot's computers is which, by name (every computer goes by one plain name).
#[ derive(Clone, Debug, Default) ]
pub struct Roles {
/// The bot's own computer: 「Master 的電腦」.
pub own : String ,
/// The computer the person is using now (where they typed to the bot), when known.
pub person : Option < String > ,
/// This AnyBot runs on a server: the computer it is on is nobody's desk.
pub server : bool ,
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
/// The tools with a `computer` choice added to each, listing the computers this bot can use.
pub fn specs_for ( computers : & [ String ] , default : & str ) -> Vec < Value > {
let mut v = specs ( ) ;
for t in v . iter_mut ( ) {
2026-11-05 13:28:32 +00:00
// Every tool that works on a computer names one, background terminals included: a command
// moved to the background on the Mac mini is read on the Mac mini.
2026-11-05 08:47:28 +00:00
// Each computer goes by its own name; the instructions say which is which. Working on
// one and calling it another (the server as "your computer") is what confused people.
let desc = format! ( " Which computer to work on, by its name. Default: {default} (the one the person chose). Use another only when the person names it; when unsure which one they mean, ask. " ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
t [ " parameters " ] [ " properties " ] [ " computer " ] = json! ( {
" type " : " string " ,
" enum " : computers ,
2026-11-05 08:03:34 +00:00
" description " : desc
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
} ) ;
}
v
}
pub fn sh_quote ( s : & str ) -> String {
format! ( " ' {} ' " , s . replace ( '\'' , " ' \\ '' " ) )
}
fn ssh_command ( target : & str , dir : & str , cmd : & str ) -> String {
let inner = if dir . trim ( ) . is_empty ( ) { cmd . to_string ( ) } else { format! ( " cd {} && {} " , sh_quote ( dir ) , cmd ) } ;
format! ( " ssh -o BatchMode=yes -o ConnectTimeout=15 {} {} " , sh_quote ( target ) , sh_quote ( & inner ) )
}
/// The local wrapper that runs `cmd` on a remote computer (an SSH host or the bot's box).
2026-11-05 07:48:20 +00:00
fn wrapper_for ( place : & Place , cmd : & str , interactive : bool , job : & str ) -> Option < String > {
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
match place {
Place ::Ssh { target , dir } = > Some ( ssh_command ( target , dir , cmd ) ) ,
2026-11-05 07:48:20 +00:00
Place ::Box { dir } = > Some ( crate ::cloudbox ::exec_wrapper ( dir , cmd , interactive , job ) ) ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
_ = > None ,
}
}
Bots work like someone who knows the computer: they know each machine, reach what they see, plan, and hand the planned steps to a quick model
Measured on the bot's own box (8 tasks x 2, grok-4.7): 14/16 done, 120 steps, 1114 s before;
16/16, 102 steps, 574 s after (model time 1015 -> 471 s). A replayed skill: 49 s -> 15 s.
Knowing the machine
- facts.rs: 電腦現況 per computer (system, desktop, systemd, sudo, network, package manager,
tools, apps, a fixed TZ), read once by a small read-only probe, kept 30 min, in the
instructions at the start of a turn.
- The box's sudo is the bot's: only what follows sudo is checked against the always-denied
list (it was refused outright, while the instructions said it could sudo), and commands with
sudo run as the box's root (setuid does nothing under gVisor).
- Box image v8 drops ENV TZ=UTC (it overrode /etc/localtime). ANYBOT_BOX_IMAGE /
ANYBOT_BOX_VOLUME give a throwaway box its own image and volume, never adopting the real one.
- Mac shell sandbox: App Store app CLIs (they carry their own app sandbox) and ps/top run
outside it; plain `osascript -e` too, asked the first time even in direct mode, refusing do
shell script, terminals, keystrokes, file reads, ObjC and the secret folders. A command the
sandbox broke says so, instead of looking like a broken program.
Reaching what it sees
- The Mac menu bar's status icons are numbered (each app's AXExtrasMenuBar, named from inside
for the system's own), computer_use_zoom looks closer at full sharpness.
- Numbers stay with their controls from look to look (they were reassigned every look, so a
step's second click landed elsewhere); the box's front window is listed first; a control can
be named instead of numbered (name "4").
- Web page numbering walks open shadow DOM (Chromium settings, web components).
- A box on a Docker host elsewhere retries the next port when the usual one is taken (a retry
without it came up with no network).
Working like a person
- Instructions: pick the route, know what done looks like, one route, check once, say what's
stuck instead of faking it with a substitute, ask before cutting the connection.
- StepPlan: the bot's model takes the first step, the step after trouble (failed, refused,
repeated, nothing changed on screen, the quick model switching routes), one in four, and the
final check; a quick model (Bot.step_model, auto: grok-4.20-0309-non-reasoning on xAI) does
the planned steps in between, falling back to the bot's model if it can't be used.
- Skills keep the steps that worked (with what each acted on, not numbers); replay_skill does
them again with no model in between, finding each control again, handing back on a mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
2026-11-09 01:01:03 +00:00
/// On the bot's own box sudo is the bot's (the box gives it sudo without a password, and the
/// instructions say it may install packages): only what follows sudo is checked against the
/// always-denied list (`sudo rm -rf /` is still refused). It was refused outright, so the bot,
/// told it could sudo, worked around a block it couldn't see the reason for.
fn denied_on_box ( cmd : & str ) -> Option < Decision > {
for part in split_commands ( cmd ) ? {
let mut rest = part . trim ( ) ;
while let Some ( after ) = rest . strip_prefix ( " sudo " ) . filter ( | a | a . is_empty ( ) | | a . starts_with ( char ::is_whitespace ) ) {
rest = after . trim_start ( ) ;
// sudo's own options (-n, -E, -H, -u root…) before the command.
while rest . starts_with ( '-' ) {
let ( opt , more ) = rest . split_once ( char ::is_whitespace ) . unwrap_or ( ( rest , " " ) ) ;
rest = more . trim_start ( ) ;
if matches! ( opt , " -u " | " -g " | " -C " | " -D " | " -h " | " -p " | " -r " | " -t " | " -U " ) {
rest = rest . split_once ( char ::is_whitespace ) . map ( | ( _ , m ) | m . trim_start ( ) ) . unwrap_or ( " " ) ;
}
}
}
if let Some ( d ) = DENIED_COMMANDS . iter ( ) . filter ( | d | * * d ! = " sudo " ) . find ( | d | starts_with_words ( rest , d ) ) {
return Some ( Decision ::Deny ( format! ( " 「 {d} 」這類指令一律不允許。 " ) ) ) ;
}
}
None
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
pub fn decide_on ( name : & str , args : & str , target : & Target , grants : & [ String ] ) -> Decision {
let v = args_of ( args ) ;
let denied = | cmd : & str | -> Option < Decision > {
for part in split_commands ( cmd ) ? {
if let Some ( d ) = DENIED_COMMANDS . iter ( ) . find ( | d | starts_with_words ( & part , d ) ) {
return Some ( Decision ::Deny ( format! ( " 「 {d} 」這類指令一律不允許。 " ) ) ) ;
}
}
None
} ;
match & target . place {
Place ::Local { workdir } = > decide ( name , args , workdir , grants ) ,
Place ::Sealed { root } = > {
if name = = " shell " {
Whole control flow checked against real chats: what the bot sees is what is there, every step lands, nothing done twice
Web
- web_fetch kept pages with a <header> only up to it (taken for <head>): articles came back as their menus.
- browser_open waits for the page to load and returns it like browser_read (one step fewer per page);
actions wait for the page to hold still instead of fixed 1-2.5 s sleeps; one way to reach the tab.
The bot's computer (box v6)
- Numbers only on what can be seen: clipped to the window and the page's visible area, none under
another app's window (a link scrolled under the toolbar was numbered on Back).
- press_key with a typed character no longer swallows the keys after it (xdotool type --args 1).
- The person's input through noVNC/TigerVNC is noticed again (fence, continuous updates, QEMU keys).
- "Nothing changed" only after clicks, scrolls and drags; long text is pasted; one screen step at a
time; one docker inspect per step when the box is known ready; poppler-utils for long PDFs.
- One typing path and fewer round trips (stdin variant, unused picture path, extra batch execs gone).
The person's computers
- Linux: the window system's front app when it has one, and the reason when there is none (was 「」);
hidden and off-screen controls get no number; wording no longer names a tool that doesn't exist.
The loop
- Without Docker, the bot's own computer runs no commands where there is no sandbox (Linux, Windows):
it was a plain shell on the brain itself, keys in reach, asked nothing.
- The bot's own computer is re-checked each step; unreachable computers are failures, not safety
blocks; no direct-mode note on every result; an empty reply is asked again; scrolling and keys
aren't counted as going in circles; steps aren't moved to a computer that can't do them.
- One finish() for every step's result, keep_grant(), dead appctl list_computers/SERVING and the
impossible server branches removed; duplicated prompt instructions merged.
Tests: 162 (Mac) / 166 (Linux) unit; real box drills 6/6 on box v6; a brain without Chrome browses
on its box; remote E2E 28/28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 09:44:59 +00:00
// Its own computer: the sandbox keeps it inside its folder. No sandbox, no command.
if ! sealed_runs_commands ( ) {
return Decision ::Deny ( NO_BOX_FOR_COMMANDS . into ( ) ) ;
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
return denied ( & arg_str ( & v , " command " ) ) . unwrap_or ( Decision ::Allow ) ;
}
if name = = " read_terminal " | | name = = " write_terminal " {
return Decision ::Allow ;
}
let p = resolve ( root , & arg_str ( & v , " path " ) ) ;
if ! inside ( root , & p ) {
2026-11-05 08:47:28 +00:00
return Decision ::Deny ( format! ( " {} 看不到這台電腦上的其他檔案( {} )。要處理使用者的檔案,請在使用者的電腦上做(不確定是哪台就先問)。 " , target . name , p . display ( ) ) ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
Decision ::Allow
}
// Its own computer: everything is allowed (it can be rebuilt), like the original's box.
Place ::Box { .. } = > {
if name = = " shell " {
Bots work like someone who knows the computer: they know each machine, reach what they see, plan, and hand the planned steps to a quick model
Measured on the bot's own box (8 tasks x 2, grok-4.7): 14/16 done, 120 steps, 1114 s before;
16/16, 102 steps, 574 s after (model time 1015 -> 471 s). A replayed skill: 49 s -> 15 s.
Knowing the machine
- facts.rs: 電腦現況 per computer (system, desktop, systemd, sudo, network, package manager,
tools, apps, a fixed TZ), read once by a small read-only probe, kept 30 min, in the
instructions at the start of a turn.
- The box's sudo is the bot's: only what follows sudo is checked against the always-denied
list (it was refused outright, while the instructions said it could sudo), and commands with
sudo run as the box's root (setuid does nothing under gVisor).
- Box image v8 drops ENV TZ=UTC (it overrode /etc/localtime). ANYBOT_BOX_IMAGE /
ANYBOT_BOX_VOLUME give a throwaway box its own image and volume, never adopting the real one.
- Mac shell sandbox: App Store app CLIs (they carry their own app sandbox) and ps/top run
outside it; plain `osascript -e` too, asked the first time even in direct mode, refusing do
shell script, terminals, keystrokes, file reads, ObjC and the secret folders. A command the
sandbox broke says so, instead of looking like a broken program.
Reaching what it sees
- The Mac menu bar's status icons are numbered (each app's AXExtrasMenuBar, named from inside
for the system's own), computer_use_zoom looks closer at full sharpness.
- Numbers stay with their controls from look to look (they were reassigned every look, so a
step's second click landed elsewhere); the box's front window is listed first; a control can
be named instead of numbered (name "4").
- Web page numbering walks open shadow DOM (Chromium settings, web components).
- A box on a Docker host elsewhere retries the next port when the usual one is taken (a retry
without it came up with no network).
Working like a person
- Instructions: pick the route, know what done looks like, one route, check once, say what's
stuck instead of faking it with a substitute, ask before cutting the connection.
- StepPlan: the bot's model takes the first step, the step after trouble (failed, refused,
repeated, nothing changed on screen, the quick model switching routes), one in four, and the
final check; a quick model (Bot.step_model, auto: grok-4.20-0309-non-reasoning on xAI) does
the planned steps in between, falling back to the bot's model if it can't be used.
- Skills keep the steps that worked (with what each acted on, not numbers); replay_skill does
them again with no model in between, finding each control again, handing back on a mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
2026-11-09 01:01:03 +00:00
if let Some ( d ) = denied_on_box ( & arg_str ( & v , " command " ) ) {
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
return d ;
}
}
Decision ::Allow
}
Place ::Ssh { .. } | Place ::Device { .. } = > match name {
" list_dir " | " read_file " | " search " | " read_terminal " | " write_terminal " = > Decision ::Allow ,
" edit_file " = > {
if grants . iter ( ) . any ( | g | g = = " edit_file " ) {
Decision ::Allow
} else {
Decision ::Ask { reason : format ! ( " 修改 {} 上的檔案:{} " , target . name , arg_str ( & v , " path " ) ) , grant : " edit_file " . into ( ) }
}
}
_ = > {
let cmd = arg_str ( & v , " command " ) ;
match decide ( " shell " , args , Path ::new ( " /nonexistent-remote " ) , grants ) {
Decision ::Ask { grant , .. } = > Decision ::Ask { reason : format ! ( " 在 {} 上執行指令:{cmd} " , target . name ) , grant } ,
other = > other ,
}
}
} ,
}
}
/// Run a remote command and give back its output (no background hand-off).
fn remote_capture ( place : & Place , cmd : & str , stdin : Option < & [ u8 ] > ) -> Result < String , String > {
use std ::io ::Write ;
2026-11-05 07:48:20 +00:00
let w = wrapper_for ( place , cmd , stdin . is_some ( ) , " " ) . ok_or ( " 不是遠端電腦 " ) ? ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let mut c = build_wrapper_command ( & w , " " , " " ) ;
let mut child = c . spawn ( ) . map_err ( | e | format! ( " 無法執行: {e} " ) ) ? ;
if let Some ( data ) = stdin {
if let Some ( mut si ) = child . stdin . take ( ) {
si . write_all ( data ) . map_err ( | e | format! ( " 寫入失敗: {e} " ) ) ? ;
}
} else {
drop ( child . stdin . take ( ) ) ;
}
let out = child . wait_with_output ( ) . map_err ( | e | e . to_string ( ) ) ? ;
let text = String ::from_utf8_lossy ( & out . stdout ) . to_string ( ) ;
if out . status . success ( ) {
Ok ( text )
} else {
Err ( format! ( " {} {} " , String ::from_utf8_lossy ( & out . stderr ) . trim ( ) , if text . trim ( ) . is_empty ( ) { String ::new ( ) } else { format! ( " \n {} " , text . trim ( ) ) } ) )
}
}
/// Run an allowed tool on a computer.
pub fn run_on ( name : & str , args : & str , target : & Target , cancel : & AtomicBool , ids : & RunIds ) -> ( String , bool ) {
let v = args_of ( args ) ;
let done = | r : Result < String , String > | match r {
Ok ( s ) = > ( s , true ) ,
Err ( e ) = > ( e , false ) ,
} ;
match & target . place {
Place ::Local { workdir } = > run ( name , args , workdir , cancel , ids ) ,
2026-11-05 13:28:32 +00:00
Place ::Device { host , dir } = > {
// The device runs the tool in the bot's work folder (its own home when unset), and
// must answer before the relay gives up at 35 s: long commands move to the
// background there, and read_terminal now names this computer to find them.
let mut v = v . clone ( ) ;
let cap = timeout_of ( & v ) . min ( DEVICE_SHELL_TIMEOUT_MS ) ;
if let Some ( o ) = v . as_object_mut ( ) {
if ! dir . is_empty ( ) & & o . get ( " workdir " ) . and_then ( Value ::as_str ) . unwrap_or ( " " ) . is_empty ( ) {
o . insert ( " workdir " . into ( ) , serde_json ::Value ::String ( dir . clone ( ) ) ) ;
}
o . insert ( " timeout_ms " . into ( ) , serde_json ::Value ::from ( cap ) ) ;
}
let args = v . to_string ( ) ;
match crate ::remote ::run_on_device ( host , name , & args , " " ) {
Ok ( v ) = > {
let mut text = v [ " text " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
if text . contains ( " read_terminal " ) {
text . push_str ( & format! ( " (這台是「 {} 」: read_terminal 的 computer 填「 {} 」。) " , target . name , target . name ) ) ;
}
( text , v [ " ok " ] . as_bool ( ) . unwrap_or ( false ) )
}
Err ( e ) = > ( e , false ) ,
}
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
Place ::Sealed { root } = > {
let _ = std ::fs ::create_dir_all ( root ) ;
Whole control flow checked against real chats: what the bot sees is what is there, every step lands, nothing done twice
Web
- web_fetch kept pages with a <header> only up to it (taken for <head>): articles came back as their menus.
- browser_open waits for the page to load and returns it like browser_read (one step fewer per page);
actions wait for the page to hold still instead of fixed 1-2.5 s sleeps; one way to reach the tab.
The bot's computer (box v6)
- Numbers only on what can be seen: clipped to the window and the page's visible area, none under
another app's window (a link scrolled under the toolbar was numbered on Back).
- press_key with a typed character no longer swallows the keys after it (xdotool type --args 1).
- The person's input through noVNC/TigerVNC is noticed again (fence, continuous updates, QEMU keys).
- "Nothing changed" only after clicks, scrolls and drags; long text is pasted; one screen step at a
time; one docker inspect per step when the box is known ready; poppler-utils for long PDFs.
- One typing path and fewer round trips (stdin variant, unused picture path, extra batch execs gone).
The person's computers
- Linux: the window system's front app when it has one, and the reason when there is none (was 「」);
hidden and off-screen controls get no number; wording no longer names a tool that doesn't exist.
The loop
- Without Docker, the bot's own computer runs no commands where there is no sandbox (Linux, Windows):
it was a plain shell on the brain itself, keys in reach, asked nothing.
- The bot's own computer is re-checked each step; unreachable computers are failures, not safety
blocks; no direct-mode note on every result; an empty reply is asked again; scrolling and keys
aren't counted as going in circles; steps aren't moved to a computer that can't do them.
- One finish() for every step's result, keep_grant(), dead appctl list_computers/SERVING and the
impossible server branches removed; duplicated prompt instructions merged.
Tests: 162 (Mac) / 166 (Linux) unit; real box drills 6/6 on box v6; a brain without Chrome browses
on its box; remote E2E 28/28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 09:44:59 +00:00
if name = = " shell " & & ! sealed_runs_commands ( ) {
return ( NO_BOX_FOR_COMMANDS . into ( ) , false ) ;
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
if name = = " shell " {
let home = dirs ::home_dir ( ) . unwrap_or_else ( | | PathBuf ::from ( " /Users " ) ) ;
return done ( shell ( & arg_str ( & v , " command " ) , root , timeout_of ( & v ) , cancel , ids , Some ( & home ) ) ) ;
}
run ( name , args , root , cancel , ids )
}
place @ ( Place ::Ssh { .. } | Place ::Box { .. } ) = > {
if let Place ::Box { .. } = place {
if let Err ( e ) = crate ::cloudbox ::ensure_running ( ) {
return ( e , false ) ;
}
}
let _busy = crate ::cloudbox ::Busy ::new ( matches! ( place , Place ::Box { .. } ) ) ;
let path = arg_str ( & v , " path " ) ;
let p = if path . is_empty ( ) { " . " . to_string ( ) } else { path . clone ( ) } ;
if v . is_null ( ) & & ! args . trim ( ) . is_empty ( ) {
return ( " 參數不是有效的 JSON " . into ( ) , false ) ;
}
2026-11-05 07:48:20 +00:00
// Box file tools stay inside this bot's own folder: every bot shares one container,
// one user and one disk, so `../` or an absolute path reached (and rewrote) the
// other bots' files — a planted .bashrc runs code on their next command.
let ( p , path ) = match place {
Place ::Box { dir } if name ! = " shell " = > match box_path ( std ::path ::Path ::new ( dir ) , & p ) {
Ok ( s ) = > ( s . clone ( ) , s ) ,
Err ( e ) = > return ( e , false ) ,
} ,
_ = > ( p , path ) ,
} ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let remote = match name {
" shell " if arg_str ( & v , " command " ) . trim ( ) . is_empty ( ) = > return ( " 請給要執行的指令 " . into ( ) , false ) ,
" shell " = > arg_str ( & v , " command " ) ,
" list_dir " = > format! ( " ls -la {} " , sh_quote ( & p ) ) ,
" read_file " = > {
let from = v . get ( " offset " ) . and_then ( Value ::as_u64 ) . unwrap_or ( 1 ) . max ( 1 ) ;
let n = v . get ( " limit " ) . and_then ( Value ::as_u64 ) . unwrap_or ( READ_DEFAULT_LINES as u64 ) ;
format! ( " test -f {q} || {{ echo '找不到檔案:' {q} >&2; exit 1; }} ; sed -n ' {from} , {} p' {q} | cat -n " , from + n - 1 , q = sh_quote ( & p ) )
}
" search " = > {
let flag = if v . get ( " files_only " ) . and_then ( Value ::as_bool ) . unwrap_or ( false ) { " l " } else { " n " } ;
format! ( " (command -v rg >/dev/null && rg - {flag} --no-heading -- {pat} {p} || grep -rI {flag} -- {pat} {p} ) | head -n {SEARCH_CLIENT_LIMIT_LINES} " , pat = sh_quote ( & arg_str ( & v , " pattern " ) ) , p = sh_quote ( & p ) )
}
" edit_file " = > {
if path . is_empty ( ) {
return ( " 請給檔案路徑 " . into ( ) , false ) ;
}
let old = arg_str ( & v , " old_text " ) ;
let new = arg_str ( & v , " new_text " ) ;
let q = sh_quote ( & path ) ;
let content = if old . is_empty ( ) {
new
} else {
let cur = match remote_capture ( place , & format! ( " cat {q} " ) , None ) {
Ok ( c ) = > c ,
Err ( e ) = > return ( format! ( " 讀不到 {path} : {e} " ) , false ) ,
} ;
match cur . matches ( old . as_str ( ) ) . count ( ) {
0 = > return ( " 找不到 old_text, 檔案沒有改。請先讀檔確認原文。 " . into ( ) , false ) ,
1 = > cur . replacen ( old . as_str ( ) , & new , 1 ) ,
n = > return ( format! ( " old_text 出現了 {n} 次,請多給一點前後文讓它只出現一次。 " ) , false ) ,
}
} ;
let write = format! ( " mkdir -p \" $(dirname {q} ) \" && cat > {q} " ) ;
return match remote_capture ( place , & write , Some ( content . as_bytes ( ) ) ) {
Ok ( _ ) = > ( format! ( " 已寫入 {path} ( {} 行)。 " , content . lines ( ) . count ( ) ) , true ) ,
Err ( e ) = > ( format! ( " 寫不進 {path} : {e} " ) , false ) ,
} ;
}
" read_terminal " | " write_terminal " = > return run ( name , args , Path ::new ( " / " ) , cancel , ids ) ,
other = > return ( format! ( " 沒有「 {other} 」這個工具 " ) , false ) ,
} ;
2026-11-05 07:48:20 +00:00
// Tag the in-container command so stopping it here also stops it inside the box
// (killing the local docker client alone left the program running in the box).
let job = crate ::httpd ::random_hex ( 8 ) ;
let wrapper = wrapper_for ( place , & remote , name = = " shell " , & job ) . unwrap_or_default ( ) ;
let kill = matches! ( place , Place ::Box { .. } ) . then ( | | crate ::cloudbox ::exec_kill_argv ( & job ) ) ;
done ( shell_wrapped ( & wrapper , & remote , timeout_of ( & v ) , cancel , ids , kill ) )
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
}
}
2026-11-05 07:48:20 +00:00
/// Keep a Box file path inside the bot's own folder: relative paths join the bot's dir, and
/// a normalised result that lands outside it (`../`, absolute paths to other bots' folders)
/// is refused. All bots share one container and one user, so the folder is the only wall.
pub fn box_path ( dir : & Path , p : & str ) -> Result < String , String > {
let joined = if p . starts_with ( '/' ) { PathBuf ::from ( p ) } else { dir . join ( p ) } ;
let mut parts : Vec < std ::ffi ::OsString > = vec! [ ] ;
for c in joined . components ( ) {
use std ::path ::Component ::* ;
match c {
ParentDir = > {
parts . pop ( ) ;
}
CurDir = > { }
RootDir = > parts . push ( std ::ffi ::OsString ::from ( " / " ) ) ,
Normal ( x ) = > parts . push ( x . to_os_string ( ) ) ,
Prefix ( x ) = > parts . push ( x . as_os_str ( ) . to_os_string ( ) ) ,
}
}
let mut norm = PathBuf ::new ( ) ;
for x in parts {
norm . push ( x ) ;
}
if norm . starts_with ( dir ) {
Ok ( norm . display ( ) . to_string ( ) )
} else {
Err ( format! ( " 這個檔案不在你的資料夾裡( Bot 的電腦上每個 Bot 只用自己的資料夾): {p} " ) )
}
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
/// Run a tool that the rules already allowed. Returns (output, ok).
pub fn run ( name : & str , args : & str , workdir : & Path , cancel : & AtomicBool , ids : & RunIds ) -> ( String , bool ) {
let v = args_of ( args ) ;
if v . is_null ( ) & & ! args . trim ( ) . is_empty ( ) {
return ( " 參數不是有效的 JSON " . into ( ) , false ) ;
}
let _ = std ::fs ::create_dir_all ( workdir ) ;
let r = match name {
" list_dir " = > list_dir ( & resolve ( workdir , & arg_str ( & v , " path " ) ) ) ,
" search " = > search ( & arg_str ( & v , " pattern " ) , & resolve ( workdir , & arg_str ( & v , " path " ) ) , v . get ( " files_only " ) . and_then ( Value ::as_bool ) . unwrap_or ( false ) ) ,
" read_file " = > read_file (
& resolve ( workdir , & arg_str ( & v , " path " ) ) ,
v . get ( " offset " ) . and_then ( Value ::as_u64 ) . unwrap_or ( 1 ) . max ( 1 ) as usize ,
v . get ( " limit " ) . and_then ( Value ::as_u64 ) . map ( | l | l as usize ) . unwrap_or ( READ_DEFAULT_LINES ) ,
) ,
" edit_file " = > edit_file ( & resolve ( workdir , & arg_str ( & v , " path " ) ) , & arg_str ( & v , " old_text " ) , & arg_str ( & v , " new_text " ) ) ,
" shell " = > shell ( & arg_str ( & v , " command " ) , workdir , timeout_of ( & v ) , cancel , ids , None ) ,
" read_terminal " = > read_terminal ( v . get ( " id " ) . and_then ( Value ::as_u64 ) . unwrap_or ( 0 ) ) ,
" write_terminal " = > write_terminal ( v . get ( " id " ) . and_then ( Value ::as_u64 ) . unwrap_or ( 0 ) , & arg_str ( & v , " input " ) , v . get ( " close " ) . and_then ( Value ::as_bool ) . unwrap_or ( false ) ) ,
other = > Err ( format! ( " 沒有「 {other} 」這個工具 " ) ) ,
} ;
match r {
Ok ( s ) = > ( s , true ) ,
Err ( e ) = > ( e , false ) ,
}
}
fn list_dir ( p : & Path ) -> Result < String , String > {
let rd = std ::fs ::read_dir ( p ) . map_err ( | e | format! ( " 無法開啟資料夾 {} : {e} " , p . display ( ) ) ) ? ;
let mut rows : Vec < ( bool , String , u64 ) > = rd
. filter_map ( | e | e . ok ( ) )
. filter ( | e | ! is_ignored ( & e . path ( ) ) )
. map ( | e | {
let md = e . metadata ( ) . ok ( ) ;
( md . as_ref ( ) . map ( | m | m . is_dir ( ) ) . unwrap_or ( false ) , e . file_name ( ) . to_string_lossy ( ) . to_string ( ) , md . map ( | m | m . len ( ) ) . unwrap_or ( 0 ) )
} )
. collect ( ) ;
rows . sort_by ( | a , b | b . 0. cmp ( & a . 0 ) . then ( a . 1. to_lowercase ( ) . cmp ( & b . 1. to_lowercase ( ) ) ) ) ;
let total = rows . len ( ) ;
let mut out : Vec < String > = rows . into_iter ( ) . take ( SEARCH_CLIENT_LIMIT_LINES ) . map ( | ( d , n , s ) | if d { format! ( " {n} / " ) } else { format! ( " {n} {} " , human_size ( s ) ) } ) . collect ( ) ;
if total > SEARCH_CLIENT_LIMIT_LINES {
out . push ( format! ( " …還有 {} 項 " , total - SEARCH_CLIENT_LIMIT_LINES ) ) ;
}
if out . is_empty ( ) {
out . push ( " (空資料夾) " . into ( ) ) ;
}
Ok ( format! ( " {} \n {} " , p . display ( ) , out . join ( " \n " ) ) )
}
fn human_size ( b : u64 ) -> String {
match b {
0 ..= 1023 = > format! ( " {b} B " ) ,
1024 ..= 1_048_575 = > format! ( " {:.1} KB " , b as f64 / 1024.0 ) ,
_ = > format! ( " {:.1} MB " , b as f64 / 1_048_576.0 ) ,
}
}
fn search ( pattern : & str , root : & Path , files_only : bool ) -> Result < String , String > {
if pattern . is_empty ( ) {
return Err ( " 請給要搜尋的文字 " . into ( ) ) ;
}
let deadline = Instant ::now ( ) + Duration ::from_millis ( SEARCH_TIMEOUT_MS ) ;
let mut lines : Vec < String > = vec! [ ] ;
let mut timed_out = false ;
// Prefer ripgrep when it's installed; fall back to a plain walk with substring matching.
let rg = Command ::new ( " rg " )
. args ( [ " --no-heading " , " --line-number " , " --color " , " never " , " --max-columns " , " 300 " , " -g " , " !.git " , " -g " , " !.env* " , " -g " , " !*.pem " , " -g " , " !*.key " ] )
. args ( if files_only { vec! [ " -l " ] } else { vec! [ ] } )
. arg ( " -- " )
. arg ( pattern )
. arg ( root )
. stdout ( Stdio ::piped ( ) )
. stderr ( Stdio ::null ( ) )
. spawn ( ) ;
if let Ok ( mut child ) = rg {
let mut out = child . stdout . take ( ) . unwrap ( ) ;
let buf = Arc ::new ( Mutex ::new ( Vec ::new ( ) ) ) ;
let b2 = buf . clone ( ) ;
let reader = std ::thread ::spawn ( move | | {
let mut chunk = [ 0 u8 ; 8192 ] ;
while let Ok ( n ) = out . read ( & mut chunk ) {
if n = = 0 {
break ;
}
let mut b = b2 . lock ( ) . unwrap ( ) ;
b . extend_from_slice ( & chunk [ .. n ] ) ;
if b . iter ( ) . filter ( | c | * * c = = b '\n' ) . count ( ) > SEARCH_HARD_MAX_LINES {
break ;
}
}
} ) ;
loop {
if child . try_wait ( ) . ok ( ) . flatten ( ) . is_some ( ) | | reader . is_finished ( ) {
break ;
}
if Instant ::now ( ) > deadline {
timed_out = true ;
let _ = child . kill ( ) ;
break ;
}
std ::thread ::sleep ( Duration ::from_millis ( 20 ) ) ;
}
let _ = child . kill ( ) ;
let _ = reader . join ( ) ;
let text = String ::from_utf8_lossy ( & buf . lock ( ) . unwrap ( ) ) . to_string ( ) ;
lines = text . lines ( ) . map ( | l | l . to_string ( ) ) . collect ( ) ;
} else {
let needle = pattern . to_lowercase ( ) ;
let mut stack = vec! [ root . to_path_buf ( ) ] ;
' walk : while let Some ( p ) = stack . pop ( ) {
if Instant ::now ( ) > deadline {
timed_out = true ;
break ;
}
if is_ignored ( & p ) {
continue ;
}
if p . is_dir ( ) {
if let Ok ( rd ) = std ::fs ::read_dir ( & p ) {
stack . extend ( rd . filter_map ( | e | e . ok ( ) ) . map ( | e | e . path ( ) ) . filter ( | q | q . file_name ( ) . map ( | n | n ! = " node_modules " & & n ! = " target " ) . unwrap_or ( true ) ) ) ;
}
continue ;
}
let Ok ( text ) = std ::fs ::read_to_string ( & p ) else { continue } ;
for ( i , l ) in text . lines ( ) . enumerate ( ) {
if l . to_lowercase ( ) . contains ( & needle ) {
lines . push ( if files_only { p . display ( ) . to_string ( ) } else { format! ( " {} : {} : {} " , p . display ( ) , i + 1 , l . chars ( ) . take ( 300 ) . collect ::< String > ( ) ) } ) ;
if files_only {
break ;
}
if lines . len ( ) > = SEARCH_HARD_MAX_LINES {
break 'walk ;
}
}
}
}
}
let total = lines . len ( ) ;
let mut out : Vec < String > = lines . into_iter ( ) . take ( SEARCH_CLIENT_LIMIT_LINES ) . collect ( ) ;
if total > SEARCH_CLIENT_LIMIT_LINES {
out . push ( format! ( " …結果太多,只列出前 {SEARCH_CLIENT_LIMIT_LINES} 行(共 {total} 行以上) " ) ) ;
}
if timed_out {
out . push ( format! ( " …搜尋超過 {} 秒,結果可能不完整 " , SEARCH_TIMEOUT_MS / 1000 ) ) ;
}
if out . is_empty ( ) {
out . push ( " 沒有找到 " . into ( ) ) ;
}
Ok ( out . join ( " \n " ) )
}
fn read_file ( p : & Path , offset : usize , limit : usize ) -> Result < String , String > {
let md = std ::fs ::metadata ( p ) . map_err ( | e | format! ( " 找不到 {} : {e} " , p . display ( ) ) ) ? ;
if md . is_dir ( ) {
return Err ( format! ( " {} 是資料夾,請用 list_dir " , p . display ( ) ) ) ;
}
if md . len ( ) > READ_MAX_BYTES {
return Err ( format! ( " {} 太大了( {} ),最多讀 8 MB " , p . display ( ) , human_size ( md . len ( ) ) ) ) ;
}
let bytes = std ::fs ::read ( p ) . map_err ( | e | format! ( " 無法讀取 {} : {e} " , p . display ( ) ) ) ? ;
if bytes . iter ( ) . take ( 8000 ) . any ( | b | * b = = 0 ) {
return Err ( format! ( " {} 不是文字檔( {} ) " , p . display ( ) , human_size ( md . len ( ) ) ) ) ;
}
let text = String ::from_utf8_lossy ( & bytes ) ;
let all : Vec < & str > = text . lines ( ) . collect ( ) ;
let end = ( offset - 1 + limit ) . min ( all . len ( ) ) ;
let mut out : Vec < String > = all . iter ( ) . enumerate ( ) . skip ( offset - 1 ) . take ( end . saturating_sub ( offset - 1 ) ) . map ( | ( i , l ) | format! ( " {:>5} {l} " , i + 1 ) ) . collect ( ) ;
if end < all . len ( ) {
out . push ( format! ( " …還有 {} 行(用 offset= {} 繼續讀) " , all . len ( ) - end , end + 1 ) ) ;
}
if out . is_empty ( ) {
out . push ( " (空檔案) " . into ( ) ) ;
}
Ok ( out . join ( " \n " ) )
}
fn edit_file ( p : & Path , old : & str , new : & str ) -> Result < String , String > {
if old . is_empty ( ) {
if let Some ( dir ) = p . parent ( ) {
std ::fs ::create_dir_all ( dir ) . map_err ( | e | format! ( " 無法建立資料夾: {e} " ) ) ? ;
}
let existed = p . exists ( ) ;
std ::fs ::write ( p , new ) . map_err ( | e | format! ( " 無法寫入 {} : {e} " , p . display ( ) ) ) ? ;
return Ok ( format! ( " {} {} ( {} 行) " , if existed { " 已覆寫 " } else { " 已建立 " } , p . display ( ) , new . lines ( ) . count ( ) ) ) ;
}
let text = std ::fs ::read_to_string ( p ) . map_err ( | e | format! ( " 無法讀取 {} : {e} " , p . display ( ) ) ) ? ;
match text . matches ( old ) . count ( ) {
0 = > Err ( format! ( " 在 {} 裡找不到要替換的文字 " , p . display ( ) ) ) ,
1 = > {
std ::fs ::write ( p , text . replacen ( old , new , 1 ) ) . map_err ( | e | format! ( " 無法寫入 {} : {e} " , p . display ( ) ) ) ? ;
Ok ( format! ( " 已修改 {} " , p . display ( ) ) )
}
n = > Err ( format! ( " 要替換的文字在 {} 出現了 {n} 次,請多給一些前後文讓它只出現一次 " , p . display ( ) ) ) ,
}
}
fn shell ( cmd : & str , workdir : & Path , timeout_ms : u64 , cancel : & AtomicBool , ids : & RunIds , seal : Option < & Path > ) -> Result < String , String > {
Bots work like someone who knows the computer: they know each machine, reach what they see, plan, and hand the planned steps to a quick model
Measured on the bot's own box (8 tasks x 2, grok-4.7): 14/16 done, 120 steps, 1114 s before;
16/16, 102 steps, 574 s after (model time 1015 -> 471 s). A replayed skill: 49 s -> 15 s.
Knowing the machine
- facts.rs: 電腦現況 per computer (system, desktop, systemd, sudo, network, package manager,
tools, apps, a fixed TZ), read once by a small read-only probe, kept 30 min, in the
instructions at the start of a turn.
- The box's sudo is the bot's: only what follows sudo is checked against the always-denied
list (it was refused outright, while the instructions said it could sudo), and commands with
sudo run as the box's root (setuid does nothing under gVisor).
- Box image v8 drops ENV TZ=UTC (it overrode /etc/localtime). ANYBOT_BOX_IMAGE /
ANYBOT_BOX_VOLUME give a throwaway box its own image and volume, never adopting the real one.
- Mac shell sandbox: App Store app CLIs (they carry their own app sandbox) and ps/top run
outside it; plain `osascript -e` too, asked the first time even in direct mode, refusing do
shell script, terminals, keystrokes, file reads, ObjC and the secret folders. A command the
sandbox broke says so, instead of looking like a broken program.
Reaching what it sees
- The Mac menu bar's status icons are numbered (each app's AXExtrasMenuBar, named from inside
for the system's own), computer_use_zoom looks closer at full sharpness.
- Numbers stay with their controls from look to look (they were reassigned every look, so a
step's second click landed elsewhere); the box's front window is listed first; a control can
be named instead of numbered (name "4").
- Web page numbering walks open shadow DOM (Chromium settings, web components).
- A box on a Docker host elsewhere retries the next port when the usual one is taken (a retry
without it came up with no network).
Working like a person
- Instructions: pick the route, know what done looks like, one route, check once, say what's
stuck instead of faking it with a substitute, ask before cutting the connection.
- StepPlan: the bot's model takes the first step, the step after trouble (failed, refused,
repeated, nothing changed on screen, the quick model switching routes), one in four, and the
final check; a quick model (Bot.step_model, auto: grok-4.20-0309-non-reasoning on xAI) does
the planned steps in between, falling back to the bot's model if it can't be used.
- Skills keep the steps that worked (with what each acted on, not numbers); replay_skill does
them again with no model in between, finding each control again, handing back on a mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
2026-11-09 01:01:03 +00:00
let r = run_command ( build_command_sealed ( cmd , workdir , & protected_dirs ( ) , & readable_dirs ( ) , seal , & ids . conversation , & ids . request ) , cmd , timeout_ms , cancel , ids , None ) ;
// (The bot's own sealed computer keeps every command in the sandbox: no hints about outside.)
let note = | out : & str | sandbox_note ( cmd , out ) . filter ( | _ | seal . is_none ( ) ) . map ( | n | format! ( " {out} \n {n} " ) ) . unwrap_or_else ( | | out . to_string ( ) ) ;
match r {
Ok ( out ) = > Ok ( note ( & out ) ) ,
Err ( out ) = > Err ( note ( & out ) ) ,
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
/// Run a wrapper command (ssh / docker exec) the same way: background hand-off, stop, output.
2026-11-05 07:48:20 +00:00
pub fn shell_wrapped ( wrapper : & str , shown : & str , timeout_ms : u64 , cancel : & AtomicBool , ids : & RunIds , kill : Option < Vec < String > > ) -> Result < String , String > {
run_command ( build_wrapper_command ( wrapper , & ids . conversation , & ids . request ) , shown , timeout_ms , cancel , ids , kill )
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
2026-11-05 07:48:20 +00:00
fn run_command ( mut command : Command , cmd : & str , timeout_ms : u64 , cancel : & AtomicBool , ids : & RunIds , kill : Option < Vec < String > > ) -> Result < String , String > {
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let mut child = command . spawn ( ) . map_err ( | e | format! ( " 無法執行: {e} " ) ) ? ;
let stdin = Arc ::new ( Mutex ::new ( child . stdin . take ( ) ) ) ;
let root = child . id ( ) ;
let output = Arc ::new ( Mutex ::new ( String ::new ( ) ) ) ;
for stream in [ child . stdout . take ( ) . map ( | s | Box ::new ( s ) as Box < dyn Read + Send > ) , child . stderr . take ( ) . map ( | s | Box ::new ( s ) as Box < dyn Read + Send > ) ] . into_iter ( ) . flatten ( ) {
let out = output . clone ( ) ;
std ::thread ::spawn ( move | | {
let mut stream = stream ;
let mut chunk = [ 0 u8 ; 4096 ] ;
while let Ok ( n ) = stream . read ( & mut chunk ) {
if n = = 0 {
break ;
}
2026-11-05 07:48:20 +00:00
// Cap the kept output (keep the newest): `yes` must not eat all the RAM.
let mut out = out . lock ( ) . unwrap ( ) ;
if out . len ( ) < 4_000_000 {
out . push_str ( & String ::from_utf8_lossy ( & chunk [ .. n ] ) ) ;
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
} ) ;
}
let start = Instant ::now ( ) ;
loop {
if let Ok ( Some ( status ) ) = child . try_wait ( ) {
std ::thread ::sleep ( Duration ::from_millis ( 30 ) ) ;
let out = head_tail ( & output . lock ( ) . unwrap ( ) ) ;
let code = status . code ( ) . unwrap_or ( - 1 ) ;
return if code = = 0 { Ok ( if out . trim ( ) . is_empty ( ) { " (完成,沒有輸出) " . into ( ) } else { out } ) } else { Err ( format! ( " 結束代碼 {code} \n {out} " ) ) } ;
}
if cancel . load ( Ordering ::Relaxed ) {
kill_tree ( root ) ;
let _ = child . kill ( ) ;
2026-11-05 07:48:20 +00:00
// Reap it: a killed child nobody waits for stays a zombie for the app's lifetime.
let _ = child . wait ( ) ;
// The remote end (a program running inside the box) is not in this process tree.
if let Some ( k ) = & kill {
if let Some ( ( head , rest ) ) = k . split_first ( ) {
let _ = Command ::new ( head ) . args ( rest ) . status ( ) ;
}
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
return Err ( " 已停止(連同它啟動的程式) " . into ( ) ) ;
}
if start . elapsed ( ) > = Duration ::from_millis ( timeout_ms ) {
// Hand off to the background instead of killing it.
let id = next_terminal_id ( ) ;
let child = Arc ::new ( Mutex ::new ( child ) ) ;
let exit = Arc ::new ( Mutex ::new ( None ) ) ;
let ( c2 , e2 ) = ( child . clone ( ) , exit . clone ( ) ) ;
std ::thread ::spawn ( move | | loop {
if let Ok ( Some ( st ) ) = c2 . lock ( ) . unwrap ( ) . try_wait ( ) {
* e2 . lock ( ) . unwrap ( ) = Some ( st . code ( ) . unwrap_or ( - 1 ) ) ;
break ;
}
std ::thread ::sleep ( Duration ::from_millis ( 200 ) ) ;
} ) ;
let so_far = head_tail ( & output . lock ( ) . unwrap ( ) ) ;
terminals ( ) . lock ( ) . unwrap ( ) . insert ( id , Terminal { command : cmd . to_string ( ) , stdin , output , child , exit , root , conversation : ids . conversation . clone ( ) } ) ;
return Ok ( format! ( " 指令跑了超過 {} 秒,已移到背景繼續執行(背景指令 # {id} )。用 read_terminal 查看進度。 \n 目前的輸出: \n {so_far} " , timeout_ms / 1000 ) ) ;
}
std ::thread ::sleep ( Duration ::from_millis ( 20 ) ) ;
}
}
fn read_terminal ( id : u64 ) -> Result < String , String > {
let t = terminals ( ) . lock ( ) . unwrap ( ) ;
let term = t . get ( & id ) . ok_or_else ( | | format! ( " 沒有背景指令 # {id} " ) ) ? ;
let out = head_tail ( & term . output . lock ( ) . unwrap ( ) ) ;
let state = match * term . exit . lock ( ) . unwrap ( ) {
Some ( c ) = > format! ( " 已結束(代碼 {c} ) " ) ,
None = > {
let ( n , cpu , mem ) = tree_stats ( term . root ) ;
format! ( " 還在執行: {n} 個程式, CPU 時間 {:.1} 秒,記憶體 {mem} MB " , cpu as f64 / 1000.0 )
}
} ;
Ok ( format! ( " 背景指令 # {id} : {} \n 狀態: {state} \n {out} " , term . command ) )
}
/// Send input to a background command (like typing into its terminal). `close` ends its input.
fn write_terminal ( id : u64 , input : & str , close : bool ) -> Result < String , String > {
use std ::io ::Write ;
2026-11-05 07:48:20 +00:00
// Copy the terminal's own handles out first: writing to a pipe whose buffer is full blocks,
// and holding the shared registry lock while blocked stops every other terminal tool.
let ( stdin , exit ) = {
let t = terminals ( ) . lock ( ) . unwrap ( ) ;
let term = t . get ( & id ) . ok_or_else ( | | format! ( " 沒有背景指令 # {id} " ) ) ? ;
( term . stdin . clone ( ) , term . exit . clone ( ) )
} ;
if exit . lock ( ) . unwrap ( ) . is_some ( ) {
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
return Err ( format! ( " 背景指令 # {id} 已經結束了 " ) ) ;
}
2026-11-05 07:48:20 +00:00
let mut slot = stdin . lock ( ) . unwrap ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
if ! input . is_empty ( ) {
let w = slot . as_mut ( ) . ok_or ( " 這個指令的輸入已經關閉了 " ) ? ;
w . write_all ( input . as_bytes ( ) ) . and_then ( | _ | w . flush ( ) ) . map_err ( | e | format! ( " 送不進去: {e} " ) ) ? ;
}
if close {
* slot = None ;
}
drop ( slot ) ;
std ::thread ::sleep ( Duration ::from_millis ( 400 ) ) ;
let out = read_terminal ( id ) ? ;
Ok ( format! ( " 已送出 {} {} 。 \n {out} " , if input . is_empty ( ) { String ::new ( ) } else { format! ( " {} 個字 " , input . chars ( ) . count ( ) ) } , if close { " ,並結束輸入 " } else { " " } ) )
}
/// Stop every background command and what it started (when the app quits).
pub fn stop_all ( ) {
for t in terminals ( ) . lock ( ) . unwrap ( ) . values ( ) {
2026-11-05 07:48:20 +00:00
// child before exit — the watcher thread takes them in this order; the reverse order
// here could wedge both threads forever (AB-BA).
let mut child = t . child . lock ( ) . unwrap ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
if t . exit . lock ( ) . unwrap ( ) . is_none ( ) {
kill_tree ( t . root ) ;
2026-11-05 07:48:20 +00:00
let _ = child . kill ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
}
}
/// Stop one bot's background commands (and what they started). Returns how many were running.
pub fn stop_for ( conversation : & str ) -> usize {
let mut n = 0 ;
for t in terminals ( ) . lock ( ) . unwrap ( ) . values ( ) . filter ( | t | t . conversation = = conversation ) {
2026-11-05 07:48:20 +00:00
// Same lock order as the watcher thread (child, then exit).
let mut child = t . child . lock ( ) . unwrap ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
if t . exit . lock ( ) . unwrap ( ) . is_none ( ) {
kill_tree ( t . root ) ;
2026-11-05 07:48:20 +00:00
let _ = child . kill ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
n + = 1 ;
}
}
n
}
#[ cfg(test) ]
mod tests {
use super ::* ;
fn wd ( ) -> PathBuf {
let d = std ::env ::temp_dir ( ) . join ( format! ( " anybot-test- {} " , std ::process ::id ( ) ) ) ;
std ::fs ::create_dir_all ( & d ) . unwrap ( ) ;
d
}
#[ test ]
fn splits_chains ( ) {
assert_eq! ( split_commands ( " ls -la && git status | head; echo 'a;b' " ) . unwrap ( ) , vec! [ " ls -la " , " git status " , " head " , " echo 'a;b' " ] ) ;
assert! ( split_commands ( " echo $(whoami) " ) . is_none ( ) ) ;
assert! ( split_commands ( " eval ls " ) . is_none ( ) ) ;
}
#[ test ]
fn layered_rules ( ) {
let w = wd ( ) ;
assert_eq! ( decide ( " shell " , r # "{"command":"ls && git status"}"# , & w , & [ ] ) , Decision ::Allow ) ;
assert! ( matches! ( decide ( " shell " , r # "{"command":"ls && sudo rm x"}"# , & w , & [ ] ) , Decision ::Deny ( _ ) ) ) ;
2026-11-05 13:28:32 +00:00
// Secrets are the sandbox's job; the permission layer no longer denies words that
// merely look like secret paths. Read-only commands run (cat .env is cat), and the
// sandbox hides the protected folders from them.
assert_eq! ( decide ( " shell " , r # "{"command":"cat .env"}"# , & w , & [ ] ) , Decision ::Allow ) ;
assert! ( matches! ( decide ( " shell " , r # "{"command":"echo $(whoami)"}"# , & w , & [ ] ) , Decision ::Ask { ref grant , .. } if grant = = " shell:echo " ) ) ;
assert_eq! ( decide ( " shell " , r # "{"command":"echo $(whoami)"}"# , & w , & [ " shell:echo " . into ( ) ] ) , Decision ::Allow ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
assert! ( matches! ( decide ( " shell " , r # "{"command":"npm install"}"# , & w , & [ ] ) , Decision ::Ask { ref grant , .. } if grant = = " shell:npm install " ) ) ;
assert_eq! ( decide ( " shell " , r # "{"command":"npm install && ls"}"# , & w , & [ " shell:npm install " . into ( ) ] ) , Decision ::Allow ) ;
assert! ( matches! ( decide ( " shell " , r # "{"command":"echo hi > out.txt"}"# , & w , & [ ] ) , Decision ::Ask { .. } ) ) ;
assert_eq! ( decide ( " read_file " , r # "{"path":"a.txt"}"# , & w , & [ ] ) , Decision ::Allow ) ;
assert! ( matches! ( decide ( " read_file " , r # "{"path":"/etc/hosts"}"# , & w , & [ ] ) , Decision ::Ask { .. } ) ) ;
assert! ( matches! ( decide ( " read_file " , r # "{"path":"../../.ssh/id_rsa"}"# , & w , & [ ] ) , Decision ::Deny ( _ ) | Decision ::Ask { .. } ) ) ;
assert! ( matches! ( decide ( " edit_file " , r # "{"path":"a.txt","new_text":"x"}"# , & w , & [ ] ) , Decision ::Ask { .. } ) ) ;
assert_eq! ( decide ( " edit_file " , r # "{"path":"a.txt","new_text":"x"}"# , & w , & [ " edit_file " . into ( ) ] ) , Decision ::Allow ) ;
}
#[ test ]
fn edit_read_search ( ) {
let w = wd ( ) ;
let c = AtomicBool ::new ( false ) ;
assert! ( run ( " edit_file " , r # "{"path":"n/a.txt","new_text":"hello\nworld\n"}"# , & w , & c , & RunIds ::default ( ) ) . 1 ) ;
assert! ( run ( " edit_file " , r # "{"path":"n/a.txt","old_text":"world","new_text":"there"}"# , & w , & c , & RunIds ::default ( ) ) . 1 ) ;
let ( r , ok ) = run ( " read_file " , r # "{"path":"n/a.txt"}"# , & w , & c , & RunIds ::default ( ) ) ;
assert! ( ok & & r . contains ( " there " ) , " {r} " ) ;
let ( s , ok ) = run ( " search " , r # "{"pattern":"there"}"# , & w , & c , & RunIds ::default ( ) ) ;
assert! ( ok & & s . contains ( " a.txt " ) , " {s} " ) ;
assert! ( ! run ( " edit_file " , r # "{"path":"n/a.txt","old_text":"missing","new_text":"x"}"# , & w , & c , & RunIds ::default ( ) ) . 1 ) ;
}
#[ test ]
fn shell_moves_to_background ( ) {
let w = wd ( ) ;
let c = AtomicBool ::new ( false ) ;
let ( r , ok ) = run ( " shell " , r # "{"command":"echo start; sleep 1; echo end","timeout_ms":200}"# , & w , & c , & RunIds ::default ( ) ) ;
assert! ( ok & & r . contains ( " 背景指令 # " ) , " {r} " ) ;
let id : u64 = r . split ( " 背景指令 # " ) . nth ( 1 ) . unwrap ( ) . split ( ') ' ) . next ( ) . unwrap ( ) . parse ( ) . unwrap ( ) ;
std ::thread ::sleep ( Duration ::from_millis ( 1500 ) ) ;
let ( t , _ ) = run ( " read_terminal " , & format! ( r # "{{"id":{id}}}"# ) , & w , & c , & RunIds ::default ( ) ) ;
assert! ( t . contains ( " end " ) & & t . contains ( " 已結束 " ) , " {t} " ) ;
let ( q , ok ) = run ( " shell " , r # "{"command":"echo quick"}"# , & w , & c , & RunIds ::default ( ) ) ;
assert! ( ok & & q . contains ( " quick " ) ) ;
}
Bots work like someone who knows the computer: they know each machine, reach what they see, plan, and hand the planned steps to a quick model
Measured on the bot's own box (8 tasks x 2, grok-4.7): 14/16 done, 120 steps, 1114 s before;
16/16, 102 steps, 574 s after (model time 1015 -> 471 s). A replayed skill: 49 s -> 15 s.
Knowing the machine
- facts.rs: 電腦現況 per computer (system, desktop, systemd, sudo, network, package manager,
tools, apps, a fixed TZ), read once by a small read-only probe, kept 30 min, in the
instructions at the start of a turn.
- The box's sudo is the bot's: only what follows sudo is checked against the always-denied
list (it was refused outright, while the instructions said it could sudo), and commands with
sudo run as the box's root (setuid does nothing under gVisor).
- Box image v8 drops ENV TZ=UTC (it overrode /etc/localtime). ANYBOT_BOX_IMAGE /
ANYBOT_BOX_VOLUME give a throwaway box its own image and volume, never adopting the real one.
- Mac shell sandbox: App Store app CLIs (they carry their own app sandbox) and ps/top run
outside it; plain `osascript -e` too, asked the first time even in direct mode, refusing do
shell script, terminals, keystrokes, file reads, ObjC and the secret folders. A command the
sandbox broke says so, instead of looking like a broken program.
Reaching what it sees
- The Mac menu bar's status icons are numbered (each app's AXExtrasMenuBar, named from inside
for the system's own), computer_use_zoom looks closer at full sharpness.
- Numbers stay with their controls from look to look (they were reassigned every look, so a
step's second click landed elsewhere); the box's front window is listed first; a control can
be named instead of numbered (name "4").
- Web page numbering walks open shadow DOM (Chromium settings, web components).
- A box on a Docker host elsewhere retries the next port when the usual one is taken (a retry
without it came up with no network).
Working like a person
- Instructions: pick the route, know what done looks like, one route, check once, say what's
stuck instead of faking it with a substitute, ask before cutting the connection.
- StepPlan: the bot's model takes the first step, the step after trouble (failed, refused,
repeated, nothing changed on screen, the quick model switching routes), one in four, and the
final check; a quick model (Bot.step_model, auto: grok-4.20-0309-non-reasoning on xAI) does
the planned steps in between, falling back to the bot's model if it can't be used.
- Skills keep the steps that worked (with what each acted on, not numbers); replay_skill does
them again with no model in between, finding each control again, handing back on a mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
2026-11-09 01:01:03 +00:00
#[ test ]
fn sandbox_breakers_are_recognized_and_nothing_else ( ) {
assert! ( process_listing ( " ps aux " ) ) ;
assert! ( process_listing ( " ps -ax | grep -i tailscale | grep -v grep " ) ) ;
assert! ( process_listing ( " /bin/ps -o pid,comm | awk '{print $1}' | head -5 " ) ) ;
assert! ( process_listing ( " top -l 1 -n 0 " ) ) ;
assert! ( ! process_listing ( " ps aux > /tmp/x " ) ) ;
assert! ( ! process_listing ( " ps aux | grep x ~/.ssh/id_rsa " ) ) ;
assert! ( ! process_listing ( " ps aux | xargs cat " ) ) ;
assert! ( ! process_listing ( " ps aux; cat ~/.ssh/id_rsa " ) ) ;
assert! ( ! process_listing ( " ps $(cat /etc/passwd) " ) ) ;
assert! ( ! process_listing ( " pss aux " ) ) ;
assert_eq! ( plain_program ( " /Applications/Tailscale.app/Contents/MacOS/Tailscale status " ) . as_deref ( ) , Some ( " /Applications/Tailscale.app/Contents/MacOS/Tailscale " ) ) ;
assert_eq! ( plain_program ( " \" /Applications/My App.app/Contents/MacOS/My App \" --help " ) . as_deref ( ) , Some ( " /Applications/My App.app/Contents/MacOS/My App " ) ) ;
assert_eq! ( plain_program ( " /Applications/T.app/Contents/MacOS/T status; cat ~/.ssh/id_rsa " ) , None ) ;
assert_eq! ( plain_program ( " /Applications/T.app/Contents/MacOS/T status | tee x " ) , None ) ;
assert! ( ! app_sandboxed_cli ( " /usr/bin/osascript " ) ) ;
assert! ( ! app_sandboxed_cli ( " /Applications/../usr/bin/osascript " ) ) ;
assert! ( ! app_sandboxed_cli ( " /Applications/Nope.app/Contents/MacOS/Nope " ) ) ;
assert! ( ! runs_outside_sandbox ( " osascript -e 'do shell script \" cat ~/.ssh/id_rsa \" ' " ) ) ;
assert! ( ! runs_outside_sandbox ( " cat ~/.ssh/id_rsa " ) ) ;
}
#[ test ]
fn sudo_is_the_bots_on_its_own_box_but_what_follows_is_still_checked ( ) {
let b = Target { name : " Bot 的電腦 " . into ( ) , place : Place ::Box { dir : " /home/bot/bot-1 " . into ( ) } } ;
let d = | c : & str | decide_on ( " shell " , & json! ( { " command " : c } ) . to_string ( ) , & b , & [ ] ) ;
for ok in [ " sudo apt-get install -y tree " , " sudo ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && echo Asia/Tokyo | sudo tee /etc/timezone " , " sudo -n -u root true " , " ls; sudo -E make install " ] {
assert! ( matches! ( d ( ok ) , Decision ::Allow ) , " {ok} " ) ;
}
for bad in [ " sudo rm -rf / " , " sudo -u root mkfs /dev/sda " , " rm -rf ~ " , " sudo sudo dd if=/dev/zero of=/dev/sda " , " su -c id " , " sudo su " ] {
assert! ( matches! ( d ( bad ) , Decision ::Deny ( _ ) ) , " {bad} " ) ;
}
// The person's own computer is unchanged: sudo stays refused there.
let w = std ::env ::temp_dir ( ) ;
assert! ( matches! ( decide ( " shell " , r # "{"command":"sudo ls"}"# , & w , & [ ] ) , Decision ::Deny ( _ ) ) ) ;
}
#[ test ]
fn applescript_runs_outside_only_when_plain_and_harmless ( ) {
assert_eq! ( plain_words ( r # "osascript -e 'tell application "Tailscale" to quit' -e "set x to \"a\" & \"b\"""# ) . unwrap ( ) , vec! [ " osascript " , " -e " , r # "tell application "Tailscale" to quit"# , " -e " , r # "set x to "a" & "b""# ] ) ;
assert! ( plain_words ( " osascript -e 'x'; cat ~/.ssh/id_rsa " ) . is_none ( ) ) ;
assert! ( plain_words ( " osascript -e \" $(cat ~/.ssh/id_rsa) \" " ) . is_none ( ) ) ;
assert! ( plain_words ( " osascript <<EOF \n x \n EOF " ) . is_none ( ) ) ;
assert! ( plain_words ( " osascript -e 'x' > /tmp/o " ) . is_none ( ) ) ;
assert! ( osascript_outside ( r # "osascript -e 'tell application "System Events" to get name of first process whose frontmost is true'"# ) . is_ok ( ) ) ;
assert! ( osascript_outside ( r # "osascript -e 'tell application "Tailscale" to activate' -e 'delay 1'"# ) . is_ok ( ) ) ;
for bad in [
r # "osascript -e 'do shell script "cat ~/.ssh/id_rsa"'"# ,
r # "osascript -e 'tell application "Terminal" to do script "id"'"# ,
r # "osascript -e 'tell application "System Events" to keystroke "rm -rf ~"'"# ,
r # "osascript -e 'read POSIX file "/Users/x/.aws/credentials"'"# ,
r # "osascript -l JavaScript -e 'ObjC.import("Foundation")'"# ,
" osascript ~/evil.scpt " ,
" osascript -i " ,
" osascript " ,
] {
assert! ( osascript_outside ( bad ) . is_err ( ) , " {bad} " ) ;
}
}
#[ test ]
fn a_command_the_sandbox_broke_says_so ( ) {
if sandbox_exec ( ) . is_none ( ) {
return ;
}
assert! ( sandbox_note ( " ps aux; echo " , " /bin/sh: /bin/ps: Operation not permitted " ) . unwrap ( ) . contains ( " pgrep " ) ) ;
let err = " 40:85: execution error: System Events發生錯誤: 發生越權取用的錯誤。 (-10004) " ;
// Joined to another command, it stayed in the sandbox: say how to write it.
assert! ( sandbox_note ( " osascript -e 'tell application \" System Events \" to …'; echo done " , err ) . unwrap ( ) . contains ( " 單獨一條 osascript " ) ) ;
// Plain and harmless, it ran outside: a -10004 then is macOS's own answer, not the sandbox.
assert_eq! ( sandbox_note ( " osascript -e 'tell application \" System Events \" to …' " , err ) , None ) ;
assert! ( sandbox_note ( " /Applications/T.app/Contents/MacOS/T status && echo " , " /bin/sh: line 1: 9823 Trace/BPT trap: 5 " ) . unwrap ( ) . contains ( " 單獨執行 " ) ) ;
assert_eq! ( sandbox_note ( " ls " , " No such file or directory " ) , None ) ;
}
/// Plain AppleScript drives another app (outside the sandbox); joined to another command it
/// stays in the sandbox and gets -10004 with a note. On a Mac with 自動化 allowed for the
/// test runner: `cargo test applescript_drives_apps -- --ignored --nocapture`.
#[ test ]
#[ ignore ]
fn applescript_drives_apps_outside_the_sandbox ( ) {
let w = std ::env ::temp_dir ( ) ;
let ask = r # "osascript -e 'tell application "System Events" to get name of first process whose frontmost is true'"# ;
let out = build_command_sealed ( ask , & w , & protected_dirs ( ) , & readable_dirs ( ) , None , " 1 " , " r " ) . output ( ) . unwrap ( ) ;
println! ( " plain: {:?} {} " , out . status , String ::from_utf8_lossy ( & out . stdout ) . trim ( ) ) ;
assert! ( out . status . success ( ) & & ! String ::from_utf8_lossy ( & out . stdout ) . trim ( ) . is_empty ( ) , " {out:?} " ) ;
let joined = format! ( " {ask} ; true " ) ;
let out = build_command_sealed ( & joined , & w , & protected_dirs ( ) , & readable_dirs ( ) , None , " 1 " , " r " ) . output ( ) . unwrap ( ) ;
let text = String ::from_utf8_lossy ( & out . stderr ) . to_string ( ) ;
println! ( " joined: {text} " ) ;
assert! ( text . contains ( " -10004 " ) ) ;
assert! ( sandbox_note ( & joined , & text ) . unwrap ( ) . contains ( " 單獨一條 osascript " ) ) ;
}
/// An App Store app's command line runs outside AnyBot's sandbox and works; inside it dies.
/// Needs Tailscale from the App Store: `cargo test app_store_cli_runs -- --ignored`.
#[ test ]
#[ ignore ]
fn app_store_cli_runs_outside_the_sandbox ( ) {
let t = " /Applications/Tailscale.app/Contents/MacOS/Tailscale " ;
assert! ( runs_outside_sandbox ( & format! ( " {t} version " ) ) ) ;
let w = std ::env ::temp_dir ( ) ;
let out = build_command_sealed ( & format! ( " {t} version " ) , & w , & protected_dirs ( ) , & readable_dirs ( ) , None , " 1 " , " r " ) . output ( ) . unwrap ( ) ;
assert! ( String ::from_utf8_lossy ( & out . stdout ) . trim ( ) . chars ( ) . next ( ) . map ( | c | c . is_ascii_digit ( ) ) . unwrap_or ( false ) , " {out:?} " ) ;
let ps = build_command_sealed ( " ps -ax | grep -c . " , & w , & protected_dirs ( ) , & readable_dirs ( ) , None , " 1 " , " r " ) . output ( ) . unwrap ( ) ;
assert! ( ps . status . success ( ) , " {ps:?} " ) ;
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
#[ test ]
fn head_tail_trims_middle ( ) {
let s = " a " . repeat ( OUTPUT_HEAD_CHARS ) + & " b " . repeat ( 5000 ) + & " c " . repeat ( OUTPUT_TAIL_CHARS ) ;
let t = head_tail ( & s ) ;
assert! ( t . contains ( " 中間省略 5000 個字 " ) ) ;
}
#[ test ]
fn cpu_time_and_tree_walk ( ) {
assert_eq! ( parse_cpu_time ( " 0:01.50 " ) , 1500 ) ;
assert_eq! ( parse_cpu_time ( " 01:02:03 " ) , 3_723_000 ) ;
assert_eq! ( parse_cpu_time ( " 1-00:00:00 " ) , 86_400_000 ) ;
let t = [
ProcRow { pid : 10 , ppid : 1 , rss_kb : 2048 , cpu_ms : 5 } ,
ProcRow { pid : 11 , ppid : 10 , rss_kb : 1024 , cpu_ms : 7 } ,
ProcRow { pid : 12 , ppid : 11 , rss_kb : 0 , cpu_ms : 0 } ,
ProcRow { pid : 13 , ppid : 1 , rss_kb : 0 , cpu_ms : 0 } ,
] ;
assert_eq! ( descendants ( & t , & [ 10 ] ) , vec! [ 10 , 11 , 12 ] ) ;
assert_eq! ( descendants ( & t , & [ 10 , 11 ] ) , vec! [ 10 , 11 , 12 ] ) ;
}
#[ test ]
fn commands_are_tagged_and_keys_hidden ( ) {
let w = wd ( ) ;
let ids = RunIds { conversation : " 7 " . into ( ) , request : " call_x " . into ( ) } ;
let ( r , ok ) = run ( " shell " , r # "{"command":"echo $ANYBOT_CONVERSATION_ID/$ANYBOT_REQUEST_ID/${OPENAI_API_KEY:-none}"}"# , & w , & AtomicBool ::new ( false ) , & ids ) ;
assert! ( ok & & r . contains ( " 7/call_x/none " ) , " {r} " ) ;
}
#[ test ]
fn sandbox_blocks_protected_folders ( ) {
if sandbox_exec ( ) . is_none ( ) {
return ;
}
let w = wd ( ) ;
let secret = std ::env ::temp_dir ( ) . join ( format! ( " anybot-secret- {} " , std ::process ::id ( ) ) ) ;
std ::fs ::create_dir_all ( & secret ) . unwrap ( ) ;
std ::fs ::write ( secret . join ( " k.txt " ) , " top-secret " ) . unwrap ( ) ;
Clippy is clean on macOS and Linux, for the app and the relay
Every warning `cargo clippy --all-targets` gave (50 on macOS, 8 more only on
Linux, 1 in the relay), fixed without changing what anything does:
- long types get a name (Peer, Members, Imported, Joining, Mention, Stream…);
- the two big enum variants are boxed (the vault's login plan, joining an
account);
- a test now waits for the copy it relaunched (it left a zombie behind);
- a hint's format had a dead `if` whose two branches were both "";
- the rest is what clippy suggests: sort_by_key, as_chunks, is_none_or,
is_multiple_of, while let, from_ref, next_back, struct init, no casts to
the same type.
Checked: cargo test (177 macOS, 181 Linux), relay tests (11), remote_e2e
28/28, and the live Linux test (X11; Wayland clicks, keys, Chinese typing,
scroll and screenshots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-07 02:10:28 +00:00
let out = build_command ( & format! ( " cat ' {} /k.txt' " , secret . display ( ) ) , & w , std ::slice ::from_ref ( & secret ) , " 1 " , " r " ) . output ( ) . unwrap ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
assert! ( ! String ::from_utf8_lossy ( & out . stdout ) . contains ( " top-secret " ) ) ;
let out = build_command ( & format! ( " cat ' {} /k.txt' " , secret . display ( ) ) , & w , & [ ] , " 1 " , " r " ) . output ( ) . unwrap ( ) ;
assert! ( String ::from_utf8_lossy ( & out . stdout ) . contains ( " top-secret " ) ) ;
2026-11-04 13:52:06 +00:00
// The chat's attachments inside it: readable, not writable; the keys next to them: hidden.
let att = secret . join ( " attachments " ) ;
std ::fs ::create_dir_all ( & att ) . unwrap ( ) ;
std ::fs ::write ( att . join ( " photo.txt " ) , " the-photo " ) . unwrap ( ) ;
Clippy is clean on macOS and Linux, for the app and the relay
Every warning `cargo clippy --all-targets` gave (50 on macOS, 8 more only on
Linux, 1 in the relay), fixed without changing what anything does:
- long types get a name (Peer, Members, Imported, Joining, Mention, Stream…);
- the two big enum variants are boxed (the vault's login plan, joining an
account);
- a test now waits for the copy it relaunched (it left a zombie behind);
- a hint's format had a dead `if` whose two branches were both "";
- the rest is what clippy suggests: sort_by_key, as_chunks, is_none_or,
is_multiple_of, while let, from_ref, next_back, struct init, no casts to
the same type.
Checked: cargo test (177 macOS, 181 Linux), relay tests (11), remote_e2e
28/28, and the live Linux test (X11; Wayland clicks, keys, Chinese typing,
scroll and screenshots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-07 02:10:28 +00:00
let run = | cmd : String | build_command_sealed ( & cmd , & w , std ::slice ::from_ref ( & secret ) , std ::slice ::from_ref ( & att ) , None , " 1 " , " r " ) . output ( ) . unwrap ( ) ;
2026-11-04 13:52:06 +00:00
assert! ( String ::from_utf8_lossy ( & run ( format! ( " cat ' {} /photo.txt' " , att . display ( ) ) ) . stdout ) . contains ( " the-photo " ) ) ;
assert! ( ! String ::from_utf8_lossy ( & run ( format! ( " cat ' {} /k.txt' " , secret . display ( ) ) ) . stdout ) . contains ( " top-secret " ) ) ;
run ( format! ( " echo changed > ' {} /photo.txt' " , att . display ( ) ) ) ;
assert_eq! ( std ::fs ::read_to_string ( att . join ( " photo.txt " ) ) . unwrap ( ) , " the-photo " ) ;
let _ = std ::fs ::remove_dir_all ( & secret ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
#[ test ]
fn bot_computer_is_sealed ( ) {
if sandbox_exec ( ) . is_none ( ) {
Whole control flow checked against real chats: what the bot sees is what is there, every step lands, nothing done twice
Web
- web_fetch kept pages with a <header> only up to it (taken for <head>): articles came back as their menus.
- browser_open waits for the page to load and returns it like browser_read (one step fewer per page);
actions wait for the page to hold still instead of fixed 1-2.5 s sleeps; one way to reach the tab.
The bot's computer (box v6)
- Numbers only on what can be seen: clipped to the window and the page's visible area, none under
another app's window (a link scrolled under the toolbar was numbered on Back).
- press_key with a typed character no longer swallows the keys after it (xdotool type --args 1).
- The person's input through noVNC/TigerVNC is noticed again (fence, continuous updates, QEMU keys).
- "Nothing changed" only after clicks, scrolls and drags; long text is pasted; one screen step at a
time; one docker inspect per step when the box is known ready; poppler-utils for long PDFs.
- One typing path and fewer round trips (stdin variant, unused picture path, extra batch execs gone).
The person's computers
- Linux: the window system's front app when it has one, and the reason when there is none (was 「」);
hidden and off-screen controls get no number; wording no longer names a tool that doesn't exist.
The loop
- Without Docker, the bot's own computer runs no commands where there is no sandbox (Linux, Windows):
it was a plain shell on the brain itself, keys in reach, asked nothing.
- The bot's own computer is re-checked each step; unreachable computers are failures, not safety
blocks; no direct-mode note on every result; an empty reply is asked again; scrolling and keys
aren't counted as going in circles; steps aren't moved to a computer that can't do them.
- One finish() for every step's result, keep_grant(), dead appctl list_computers/SERVING and the
impossible server branches removed; duplicated prompt instructions merged.
Tests: 162 (Mac) / 166 (Linux) unit; real box drills 6/6 on box v6; a brain without Chrome browses
on its box; remote E2E 28/28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 09:44:59 +00:00
// No sandbox (Linux, Windows): no command runs there at all, asked or not.
let t = Target { name : crate ::model ::BOT_COMPUTER_NAME . into ( ) , place : Place ::Sealed { root : std ::env ::temp_dir ( ) . join ( " anybot-no-seal " ) } } ;
assert! ( matches! ( decide_on ( " shell " , r # "{"command":"cat ~/.ssh/id_rsa"}"# , & t , & [ ] ) , Decision ::Deny ( _ ) ) ) ;
let ( r , ok ) = run_on ( " shell " , r # "{"command":"echo ran"}"# , & t , & AtomicBool ::new ( false ) , & RunIds ::default ( ) ) ;
assert! ( ! ok & & ! r . contains ( " ran " ) , " {r} " ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
return ;
}
let Some ( home ) = dirs ::home_dir ( ) else { return } ;
let root = home . join ( " AnyBot 工作區 " ) . join ( format! ( " test-seal- {} " , std ::process ::id ( ) ) ) ;
std ::fs ::create_dir_all ( & root ) . unwrap ( ) ;
let t = Target { name : crate ::model ::BOT_COMPUTER_NAME . into ( ) , place : Place ::Sealed { root : root . clone ( ) } } ;
let c = AtomicBool ::new ( false ) ;
let ( r , ok ) = run_on ( " shell " , r # "{"command":"echo hi > mine.txt && cat mine.txt"}"# , & t , & c , & RunIds ::default ( ) ) ;
assert! ( ok & & r . contains ( " hi " ) , " {r} " ) ;
let ( r , _ ) = run_on ( " shell " , r # "{"command":"ls ~/Documents >/dev/null 2>&1 && echo SEEN || echo HIDDEN"}"# , & t , & c , & RunIds ::default ( ) ) ;
assert! ( r . contains ( " HIDDEN " ) , " {r} " ) ;
assert! ( matches! ( decide_on ( " read_file " , r # "{"path":"~/Documents/x.txt"}"# , & t , & [ ] ) , Decision ::Deny ( _ ) ) ) ;
assert_eq! ( decide_on ( " edit_file " , r # "{"path":"a.txt","new_text":"x"}"# , & t , & [ ] ) , Decision ::Allow ) ;
let _ = std ::fs ::remove_dir_all ( & root ) ;
}
#[ test ]
fn ssh_commands_are_quoted ( ) {
assert_eq! ( ssh_command ( " me@box " , " /srv/app " , " echo 'hi' " ) , " ssh -o BatchMode=yes -o ConnectTimeout=15 'me@box' 'cd ' \\ ''/srv/app' \\ '' && echo ' \\ ''hi' \\ ''' " ) ;
let t = Target { name : " 工作站 " . into ( ) , place : Place ::Ssh { target : " me@box " . into ( ) , dir : String ::new ( ) } } ;
assert! ( matches! ( decide_on ( " shell " , r # "{"command":"npm test"}"# , & t , & [ ] ) , Decision ::Ask { ref reason , .. } if reason . contains ( " 工作站 " ) ) ) ;
assert_eq! ( decide_on ( " shell " , r # "{"command":"ls"}"# , & t , & [ ] ) , Decision ::Allow ) ;
let s = specs_for ( & [ " A " . into ( ) , " B " . into ( ) ] , " A " ) ;
assert_eq! ( s [ 0 ] [ " parameters " ] [ " properties " ] [ " computer " ] [ " enum " ] [ 1 ] , " B " ) ;
}
#[ test ]
fn stop_ends_the_whole_tree ( ) {
let w = wd ( ) ;
let cancel = Arc ::new ( AtomicBool ::new ( false ) ) ;
let c2 = cancel . clone ( ) ;
let marker = format! ( " anybot-sleep- {} " , std ::process ::id ( ) ) ;
let cmd = format! ( r # "{{"command":"sh -c 'exec -a {marker} sleep 30' & sleep 30","timeout_ms":20000}}"# ) ;
let h = std ::thread ::spawn ( move | | run ( " shell " , & cmd , & wd ( ) , & c2 , & RunIds ::default ( ) ) ) ;
std ::thread ::sleep ( Duration ::from_millis ( 800 ) ) ;
cancel . store ( true , Ordering ::Relaxed ) ;
let ( r , ok ) = h . join ( ) . unwrap ( ) ;
assert! ( ! ok & & r . contains ( " 已停止 " ) , " {r} " ) ;
std ::thread ::sleep ( Duration ::from_millis ( 300 ) ) ;
let ps = Command ::new ( " ps " ) . args ( [ " -A " , " -o " , " command= " ] ) . output ( ) . unwrap ( ) ;
assert! ( ! String ::from_utf8_lossy ( & ps . stdout ) . contains ( & marker ) , " child still running " ) ;
let _ = w ;
}
}