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
// AnyBot 帳號伺服器 (the thin backend).
//
// What it keeps: accounts (name + PBKDF2 password hash), signed-in phones (session tokens, stored
// hashed), the user's AnyBots ("hosts", tokens stored hashed), and one encrypted backup per account
// (sealed on the user's computer with a key made from the password; this server can't open it).
//
// What it does: each AnyBot keeps asking `GET /relay/host/next` (an outgoing connection, so the
// computer needs no open port). A phone signed in to the account calls `/h/<host>/api/...`; the
// request is handed to that AnyBot and its answer comes back. AnyBots of the same account can
// also reach each other (裝置連結) through `/relay/hosts/<host>/api/device/run`.
//
// Run: anybot-relay [--port 8080] [--data ./relay-data] (put HTTPS in front, e.g. Caddy)
// Sign-ups: the first account is free; after that ANYBOT_RELAY_INVITE must be set and given.
#[ path = " ../../src/httpd.rs " ]
mod httpd ;
2026-11-04 11:30:32 +00:00
#[ path = " ../../src/wire.rs " ]
mod wire ;
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
mod push ;
use httpd ::{ Req , Resp } ;
use serde ::{ Deserialize , Serialize } ;
use serde_json ::{ json , Value } ;
use std ::collections ::{ HashMap , VecDeque } ;
use std ::path ::PathBuf ;
use std ::sync ::mpsc ::{ channel , Sender } ;
2026-11-04 11:30:32 +00:00
use std ::sync ::atomic ::{ AtomicU64 , Ordering } ;
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
use std ::sync ::{ Arc , Condvar , Mutex } ;
use std ::time ::{ Duration , Instant } ;
const PAGE : & str = include_str! ( " ../../mobile/index.html " ) ;
2026-11-05 11:20:29 +00:00
const MANIFEST : & str = r ## "{"name":"AnyBot","short_name":"AnyBot","start_url":"./","display":"standalone","background_color":"#121212","theme_color":"#121212","icons":[{"src":"icon.svg","sizes":"any","type":"image/svg+xml"},{"src":"icon-192.png","sizes":"192x192","type":"image/png","purpose":"any maskable"},{"src":"icon-512.png","sizes":"512x512","type":"image/png","purpose":"any maskable"}]}"## ;
/// AnyBot's icon (the same drawing as the app's), and PNGs for the iPhone home screen and Android.
const ICON : & str = include_str! ( " ../../mobile/icon.svg " ) ;
const ICON_PNGS : [ ( & str , & [ u8 ] ) ; 3 ] = [ ( " icon-180.png " , include_bytes! ( " ../../mobile/icon-180.png " ) ) , ( " icon-192.png " , include_bytes! ( " ../../mobile/icon-192.png " ) ) , ( " icon-512.png " , include_bytes! ( " ../../mobile/icon-512.png " ) ) ] ;
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
/// How long a host's "next request?" waits before answering "nothing".
const HOST_POLL_SECS : u64 = 25 ;
/// How long a phone's request waits for the AnyBot to answer (its own long-poll is 20 s).
const FORWARD_SECS : u64 = 35 ;
2026-11-05 07:48:20 +00:00
/// Model requests relayed to a person's computer (model_fetch) are ordinary LLM latency:
/// the asking side waits minutes, so the relay must not answer first with a timeout.
const LONG_FORWARD_SECS : u64 = 610 ;
2026-11-04 12:56:06 +00:00
/// How long a 配對碼 lasts.
const PAIR_SECS : u64 = 600 ;
2026-11-04 11:07:55 +00:00
/// How long a request may wait to be picked up before the host counts as gone.
const PICKUP_SECS : u64 = 8 ;
2026-11-04 11:30:32 +00:00
/// The lasting connection: a ping this often, and given up after this long without a word.
const WS_PING_SECS : u64 = 20 ;
const WS_DEAD_SECS : u64 = 60 ;
const GONE : & str = " 那台 AnyBot 斷線了(網路中斷,或 AnyBot 關掉了) " ;
const OFFLINE : & str = " 那台 AnyBot 目前離線(電腦可能關機、睡眠、斷網,或 AnyBot 沒開) " ;
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
/// A host counts as online when it asked within this many seconds.
const ONLINE_SECS : u64 = 60 ;
/// Sessions last this long without use.
const SESSION_DAYS : i64 = 90 ;
const MAX_FAILS : u32 = 8 ;
#[ derive(Clone, Serialize, Deserialize, Default) ]
struct User {
name : String ,
pass : String ,
created : i64 ,
}
#[ derive(Clone, Serialize, Deserialize, Default) ]
struct Session {
hash : String ,
user : String ,
device : String ,
created : i64 ,
last : i64 ,
}
#[ derive(Clone, Serialize, Deserialize, Default) ]
struct Host {
id : String ,
name : String ,
user : String ,
token_hash : String ,
created : i64 ,
#[ serde(default) ]
last_seen : i64 ,
/// 團隊: other accounts this AnyBot is shared with, and what they may do.
#[ serde(default) ]
members : Vec < Member > ,
}
#[ derive(Clone, Serialize, Deserialize, Default) ]
struct Member {
user : String ,
/// "admin": everything the owner can do on the phone (approve, screens);
/// "member": chat with the bots and attach files only.
role : String ,
}
impl Host {
/// The account's role on this AnyBot (None = no access).
fn role_of ( & self , user : & str ) -> Option < String > {
if self . user = = user {
return Some ( " owner " . into ( ) ) ;
}
self . members . iter ( ) . find ( | m | m . user = = user ) . map ( | m | m . role . clone ( ) )
}
}
#[ derive(Clone, Serialize, Deserialize, Default) ]
struct Sub {
user : String ,
sub : Value ,
created : i64 ,
}
#[ derive(Serialize, Deserialize, Default) ]
struct Data {
users : Vec < User > ,
sessions : Vec < Session > ,
hosts : Vec < Host > ,
/// Phones that turned notifications on.
#[ serde(default) ]
subs : Vec < Sub > ,
}
const SW : & str = r #" self.addEventListener( " install " , () => self.skipWaiting());
self . addEventListener ( " activate " , ( e ) = > e . waitUntil ( self . clients . claim ( ) ) ) ;
self . addEventListener ( " push " , ( e ) = > {
let d = { } ;
try { d = e . data ? e . data . json ( ) : { } ; } catch ( _ ) { d = { body : e . data ? e . data . text ( ) : " " } ; }
2026-11-05 11:20:29 +00:00
e . waitUntil ( self . registration . showNotification ( d . title | | " AnyBot " , { body : d . body | | " " , tag : " bot- " + ( d . bot | | 0 ) , renotify : true , data : d , icon : " icon-192.png " , badge : " icon-192.png " } ) ) ;
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
} ) ;
self . addEventListener ( " notificationclick " , ( e ) = > {
e . notification . close ( ) ;
const d = e . notification . data | | { } ;
const url = " ./#host= " + encodeURIComponent ( d . host | | " " ) + " &bot= " + ( d . bot | | " " ) ;
e . waitUntil ( self . clients . matchAll ( { type : " window " , includeUncontrolled : true } ) . then ( ( cs ) = > {
for ( const c of cs ) { c . postMessage ( { open : d } ) ; return c . focus ( ) ; }
return self . clients . openWindow ( url ) ;
} ) ) ;
} ) ;
" #;
2026-11-04 11:30:32 +00:00
/// A forwarded request's answer.
struct Answer {
status : u16 ,
ctype : String ,
headers : Vec < ( String , String ) > ,
body : Vec < u8 > ,
/// The body is deflate-compressed (only when the asker said it can read that).
z : bool ,
}
impl Answer {
fn error ( status : u16 , msg : & str ) -> Answer {
Answer { status , ctype : " application/json; charset=utf-8 " . into ( ) , headers : vec ! [ ] , body : json ! ( { " error " : msg } ) . to_string ( ) . into_bytes ( ) , z : false }
}
fn from_json ( v : & Value ) -> Answer {
use base64 ::Engine ;
Answer {
status : v [ " status " ] . as_u64 ( ) . unwrap_or ( 502 ) as u16 ,
ctype : v [ " ctype " ] . as_str ( ) . unwrap_or ( " application/octet-stream " ) . to_string ( ) ,
headers : v [ " headers " ] . as_array ( ) . into_iter ( ) . flatten ( ) . filter_map ( | h | Some ( ( h [ 0 ] . as_str ( ) ? . to_string ( ) , h [ 1 ] . as_str ( ) ? . to_string ( ) ) ) ) . collect ( ) ,
body : base64 ::engine ::general_purpose ::STANDARD . decode ( v [ " body " ] . as_str ( ) . unwrap_or ( " " ) ) . unwrap_or_default ( ) ,
z : v [ " z " ] . as_bool ( ) . unwrap_or ( false ) ,
}
}
fn into_resp ( self ) -> Resp {
let mut resp = Resp ::bytes ( & self . ctype , self . body ) ;
resp . status = self . status ;
for ( k , v ) in self . headers {
// Only harmless headers pass through.
if k . eq_ignore_ascii_case ( " x-size " ) {
resp . headers . push ( ( k , v ) ) ;
}
}
resp
}
}
/// An AnyBot's lasting connection: frames to write to it.
struct Sock {
tx : Sender < Vec < u8 > > ,
serial : u64 ,
}
static SERIAL : AtomicU64 = AtomicU64 ::new ( 1 ) ;
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
struct Relay {
dir : PathBuf ,
data : Mutex < Data > ,
/// Requests waiting for each host.
queues : Mutex < HashMap < String , VecDeque < Value > > > ,
arrived : Condvar ,
/// Where each forwarded request's answer goes.
2026-11-05 07:48:20 +00:00
/// Requests waiting for an answer: job id → (its answer, the host it was sent to —
/// so only that host may answer it).
waiting : Mutex < HashMap < String , ( Sender < Answer > , String ) > > ,
2026-11-04 11:30:32 +00:00
/// AnyBots connected the lasting way (WebSocket), and which requests went over which.
sockets : Mutex < HashMap < String , Sock > > ,
flights : Mutex < HashMap < String , ( String , u64 ) > > ,
2026-11-04 12:56:06 +00:00
/// The brain each AnyBot shows (when it is a screen of another), and which lend their hands.
2026-11-04 11:30:32 +00:00
uses : Mutex < HashMap < String , String > > ,
2026-11-04 12:56:06 +00:00
hands : Mutex < std ::collections ::HashSet < String > > ,
/// 配對碼: code → (account, the AnyBot it is for, when made). Ten minutes, once.
pairs : Mutex < HashMap < String , ( String , String , Instant ) > > ,
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
/// When each host last asked (in memory; saved now and then).
seen : Mutex < HashMap < String , Instant > > ,
fails : Mutex < HashMap < String , ( u32 , Instant ) > > ,
invite : String ,
vapid : push ::Vapid ,
contact : String ,
}
fn now ( ) -> i64 {
chrono ::Utc ::now ( ) . timestamp ( )
}
impl Relay {
fn load ( dir : PathBuf ) -> Relay {
let _ = std ::fs ::create_dir_all ( dir . join ( " backups " ) ) ;
let data : Data = std ::fs ::read ( dir . join ( " data.json " ) ) . ok ( ) . and_then ( | b | serde_json ::from_slice ( & b ) . ok ( ) ) . unwrap_or_default ( ) ;
let vapid = push ::Vapid ::load ( & dir ) ;
Relay {
dir ,
data : Mutex ::new ( data ) ,
queues : Mutex ::new ( HashMap ::new ( ) ) ,
arrived : Condvar ::new ( ) ,
waiting : Mutex ::new ( HashMap ::new ( ) ) ,
2026-11-04 11:30:32 +00:00
sockets : Mutex ::new ( HashMap ::new ( ) ) ,
flights : Mutex ::new ( HashMap ::new ( ) ) ,
uses : Mutex ::new ( HashMap ::new ( ) ) ,
2026-11-04 12:56:06 +00:00
hands : Mutex ::new ( std ::collections ::HashSet ::new ( ) ) ,
pairs : Mutex ::new ( HashMap ::new ( ) ) ,
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
seen : Mutex ::new ( HashMap ::new ( ) ) ,
fails : Mutex ::new ( HashMap ::new ( ) ) ,
invite : std ::env ::var ( " ANYBOT_RELAY_INVITE " ) . unwrap_or_default ( ) ,
vapid ,
// Push services want a way to reach whoever runs the server.
contact : std ::env ::var ( " ANYBOT_RELAY_CONTACT " ) . ok ( ) . filter ( | c | c . starts_with ( " mailto: " ) | | c . starts_with ( " https:// " ) ) . unwrap_or_else ( | | " mailto:anybot-relay@example.com " . into ( ) ) ,
}
}
fn save ( & self , d : & Data ) {
let tmp = self . dir . join ( " data.json.tmp " ) ;
if std ::fs ::write ( & tmp , serde_json ::to_vec_pretty ( d ) . unwrap_or_default ( ) ) . is_ok ( ) {
let _ = std ::fs ::rename ( & tmp , self . dir . join ( " data.json " ) ) ;
}
}
fn session ( & self , req : & Req ) -> Option < String > {
let t = req . bearer ( ) ;
if t . is_empty ( ) {
return None ;
}
let h = httpd ::sha256_hex ( & t ) ;
let mut d = self . data . lock ( ) . unwrap ( ) ;
let cutoff = now ( ) - SESSION_DAYS * 86_400 ;
let s = d . sessions . iter_mut ( ) . find ( | s | s . hash = = h & & s . last > cutoff ) ? ;
// Count use at most once a minute (saves writing on every request).
if now ( ) - s . last > 60 {
s . last = now ( ) ;
let user = s . user . clone ( ) ;
self . save ( & d ) ;
return Some ( user ) ;
}
Some ( s . user . clone ( ) )
}
fn host ( & self , req : & Req ) -> Option < Host > {
let t = req . bearer ( ) ;
if t . is_empty ( ) {
return None ;
}
let h = httpd ::sha256_hex ( & t ) ;
let d = self . data . lock ( ) . unwrap ( ) ;
d . hosts . iter ( ) . find ( | x | x . token_hash = = h ) . cloned ( )
}
fn online ( & self , id : & str ) -> bool {
2026-11-04 11:30:32 +00:00
self . sockets . lock ( ) . unwrap ( ) . contains_key ( id ) | | self . seen . lock ( ) . unwrap ( ) . get ( id ) . map ( | t | t . elapsed ( ) . as_secs ( ) < ONLINE_SECS ) . unwrap_or ( 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
}
fn too_many_fails ( & self , key : & str ) -> bool {
let f = self . fails . lock ( ) . unwrap ( ) ;
matches! ( f . get ( key ) , Some ( ( n , t ) ) if * n > = MAX_FAILS & & t . elapsed ( ) < Duration ::from_secs ( 300 ) )
}
fn failed ( & self , key : & str ) {
let mut f = self . fails . lock ( ) . unwrap ( ) ;
let e = f . entry ( key . to_string ( ) ) . or_insert ( ( 0 , Instant ::now ( ) ) ) ;
if e . 1. elapsed ( ) > Duration ::from_secs ( 300 ) {
* e = ( 0 , Instant ::now ( ) ) ;
}
e . 0 + = 1 ;
e . 1 = Instant ::now ( ) ;
}
/// Hand a request to a host and wait for its answer.
fn forward ( & self , host_id : & str , method : & str , path : & str , query : & HashMap < String , String > , body : & [ u8 ] , who : ( & str , & str ) ) -> Resp {
2026-11-04 12:38:45 +00:00
self . forward_raw ( host_id , method , path , query , body , false , who , false , " " ) . into_resp ( )
2026-11-04 11:30:32 +00:00
}
/// `z`: the body is compressed; `z_ok`: the asker reads a compressed answer.
#[ allow(clippy::too_many_arguments) ]
2026-11-04 12:38:45 +00:00
/// `from`: the AnyBot of the account asking (empty for the phone).
fn forward_raw ( & self , host_id : & str , method : & str , path : & str , query : & HashMap < String , String > , body : & [ u8 ] , z : bool , who : ( & str , & str ) , z_ok : bool , from : & str ) -> Answer {
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
use base64 ::Engine ;
if ! self . online ( host_id ) {
2026-11-04 11:30:32 +00:00
return Answer ::error ( 503 , OFFLINE ) ;
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 id = httpd ::random_hex ( 12 ) ;
2026-11-05 07:48:20 +00:00
let secs = if path = = " /api/device/run " { LONG_FORWARD_SECS } else { FORWARD_SECS } ;
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 ( tx , rx ) = channel ( ) ;
2026-11-05 07:48:20 +00:00
self . waiting . lock ( ) . unwrap ( ) . insert ( id . clone ( ) , ( tx , host_id . to_string ( ) ) ) ;
2026-11-04 12:38:45 +00:00
let head = json! ( { " t " : " job " , " id " : id , " method " : method , " path " : path , " query " : query , " user " : who . 0 , " role " : who . 1 , " z " : z , " z_ok " : z_ok , " from " : from } ) ;
2026-11-04 11:30:32 +00:00
// Connected the lasting way: straight down that connection.
let sock = self . sockets . lock ( ) . unwrap ( ) . get ( host_id ) . map ( | s | ( s . tx . clone ( ) , s . serial ) ) ;
if let Some ( ( stx , serial ) ) = sock {
self . flights . lock ( ) . unwrap ( ) . insert ( id . clone ( ) , ( host_id . to_string ( ) , serial ) ) ;
let sent = stx . send ( wire ::pack ( & head , body ) ) . is_ok ( ) ;
2026-11-05 07:48:20 +00:00
let r = if sent { rx . recv_timeout ( Duration ::from_secs ( secs ) ) . ok ( ) } else { None } ;
2026-11-04 11:30:32 +00:00
self . waiting . lock ( ) . unwrap ( ) . remove ( & id ) ;
self . flights . lock ( ) . unwrap ( ) . remove ( & id ) ;
return match r {
Some ( a ) = > a ,
None if ! sent = > Answer ::error ( 503 , GONE ) ,
None = > Answer ::error ( 504 , " 那台 AnyBot 收到了,但太久沒有回應(可能太忙,或網路很慢) " ) ,
} ;
}
let mut job = head ;
job [ " body " ] = json! ( base64 ::engine ::general_purpose ::STANDARD . encode ( body ) ) ;
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
self . queues . lock ( ) . unwrap ( ) . entry ( host_id . to_string ( ) ) . or_default ( ) . push_back ( job ) ;
self . arrived . notify_all ( ) ;
2026-11-04 11:07:55 +00:00
// A running AnyBot always has a worker waiting, so it picks a request up within a moment.
// Still in the queue after a few seconds means it went away (closed, asleep, offline).
let mut r = rx . recv_timeout ( Duration ::from_secs ( PICKUP_SECS ) ) ;
if r . is_err ( ) {
let queued = self . queues . lock ( ) . unwrap ( ) . get ( host_id ) . map ( | q | q . iter ( ) . any ( | j | j [ " id " ] . as_str ( ) = = Some ( id . as_str ( ) ) ) ) . unwrap_or ( false ) ;
2026-11-04 11:30:32 +00:00
if ! queued {
2026-11-05 07:48:20 +00:00
r = rx . recv_timeout ( Duration ::from_secs ( secs . max ( PICKUP_SECS + 1 ) - PICKUP_SECS ) ) ;
2026-11-04 11:07:55 +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
self . waiting . lock ( ) . unwrap ( ) . remove ( & id ) ;
// Not picked up in time: drop it so it doesn't run late.
2026-11-04 11:30:32 +00:00
let was_queued = self . queues . lock ( ) . unwrap ( ) . get_mut ( host_id ) . map ( | q | {
let n = q . len ( ) ;
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
q . retain ( | j | j [ " id " ] . as_str ( ) ! = Some ( id . as_str ( ) ) ) ;
2026-11-04 11:30:32 +00:00
n ! = q . len ( )
} ) ;
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 r {
2026-11-04 11:30:32 +00:00
Ok ( a ) = > a ,
Err ( _ ) if was_queued = = Some ( true ) = > Answer ::error ( 503 , OFFLINE ) ,
Err ( _ ) = > Answer ::error ( 504 , " 那台 AnyBot 收到了,但太久沒有回應(可能太忙,或網路很慢) " ) ,
}
}
/// 長連線: an AnyBot keeps one WebSocket open; requests for it come down it at once, and its
/// own requests to the account's other AnyBots go up it.
fn ws ( self : & Arc < Self > , req : Req , mut c : std ::net ::TcpStream ) {
use std ::io ::Write ;
use tungstenite ::{ protocol ::Role , Message , WebSocket } ;
if req . path ! = " /relay/host/ws " {
return httpd ::write_response ( & mut c , & Resp ::error ( 404 , " 沒有這個網址 " ) ) ;
}
let Some ( h ) = self . host ( & req ) else { return httpd ::write_response ( & mut c , & Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) ) } ;
let key = req . header ( " sec-websocket-key " ) . to_string ( ) ;
if key . is_empty ( ) {
return httpd ::write_response ( & mut c , & Resp ::error ( 400 , " bad request " ) ) ;
}
let accept = tungstenite ::handshake ::derive_accept_key ( key . as_bytes ( ) ) ;
if c . write_all ( format! ( " HTTP/1.1 101 Switching Protocols \r \n Upgrade: websocket \r \n Connection: Upgrade \r \n Sec-WebSocket-Accept: {accept} \r \n \r \n " ) . as_bytes ( ) ) . is_err ( ) {
return ;
}
let _ = c . set_read_timeout ( Some ( Duration ::from_millis ( 40 ) ) ) ;
let _ = c . set_nodelay ( true ) ;
let cfg = tungstenite ::protocol ::WebSocketConfig ::default ( ) . max_message_size ( Some ( httpd ::MAX_BODY * 2 ) ) . max_frame_size ( Some ( httpd ::MAX_BODY * 2 ) ) ;
let mut ws = WebSocket ::from_raw_socket ( c , Role ::Server , Some ( cfg ) ) ;
let serial = SERIAL . fetch_add ( 1 , Ordering ::SeqCst ) ;
let ( tx , rx ) = channel ::< Vec < u8 > > ( ) ;
self . sockets . lock ( ) . unwrap ( ) . insert ( h . id . clone ( ) , Sock { tx : tx . clone ( ) , serial } ) ;
self . seen . lock ( ) . unwrap ( ) . insert ( h . id . clone ( ) , Instant ::now ( ) ) ;
let ( mut heard , mut pinged , mut marked ) = ( Instant ::now ( ) , Instant ::now ( ) , Instant ::now ( ) ) ;
' live : loop {
match ws . read ( ) {
Ok ( Message ::Binary ( b ) ) = > {
heard = Instant ::now ( ) ;
self . on_frame ( & h , & b , & tx ) ;
}
Ok ( Message ::Close ( _ ) ) = > break ,
Ok ( _ ) = > heard = Instant ::now ( ) ,
Err ( tungstenite ::Error ::Io ( e ) ) if matches! ( e . kind ( ) , std ::io ::ErrorKind ::WouldBlock | std ::io ::ErrorKind ::TimedOut ) = > { }
Err ( _ ) = > break ,
}
while let Ok ( f ) = rx . try_recv ( ) {
if ws . write ( Message ::Binary ( f . into ( ) ) ) . is_err ( ) {
break 'live ;
}
}
if pinged . elapsed ( ) . as_secs ( ) > = WS_PING_SECS {
pinged = Instant ::now ( ) ;
if ws . write ( Message ::Ping ( Vec ::new ( ) . into ( ) ) ) . is_err ( ) {
break ;
}
}
match ws . flush ( ) {
Err ( tungstenite ::Error ::Io ( e ) ) if matches! ( e . kind ( ) , std ::io ::ErrorKind ::WouldBlock | std ::io ::ErrorKind ::TimedOut ) = > { }
Err ( _ ) = > break ,
Ok ( ( ) ) = > { }
}
if heard . elapsed ( ) . as_secs ( ) > = WS_DEAD_SECS {
break ;
}
if marked . elapsed ( ) . as_secs ( ) > = 10 {
marked = Instant ::now ( ) ;
self . seen . lock ( ) . unwrap ( ) . insert ( h . id . clone ( ) , Instant ::now ( ) ) ;
}
}
// Gone: whoever waits on it hears so now, not after a timeout.
{
let mut socks = self . sockets . lock ( ) . unwrap ( ) ;
if socks . get ( & h . id ) . map ( | s | s . serial ) = = Some ( serial ) {
socks . remove ( & h . id ) ;
self . seen . lock ( ) . unwrap ( ) . remove ( & h . id ) ;
}
}
let lost : Vec < String > = self . flights . lock ( ) . unwrap ( ) . iter ( ) . filter ( | ( _ , ( host , s ) ) | * host = = h . id & & * s = = serial ) . map ( | ( id , _ ) | id . clone ( ) ) . collect ( ) ;
for id in lost {
2026-11-05 07:48:20 +00:00
if let Some ( ( w , _ ) ) = self . waiting . lock ( ) . unwrap ( ) . remove ( & id ) {
2026-11-04 11:30:32 +00:00
let _ = w . send ( Answer ::error ( 503 , GONE ) ) ;
}
}
}
fn on_frame ( self : & Arc < Self > , h : & Host , b : & [ u8 ] , tx : & Sender < Vec < u8 > > ) {
let Some ( ( head , body ) ) = wire ::unpack ( b ) else { return } ;
let s = | k : & str | head [ k ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
match head [ " t " ] . as_str ( ) . unwrap_or ( " " ) {
" reply " = > {
2026-11-05 07:48:20 +00:00
// Only the host the job went to may answer it.
let w = {
let mut waiting = self . waiting . lock ( ) . unwrap ( ) ;
match waiting . get ( & s ( " id " ) ) {
Some ( ( _ , target ) ) if * target = = h . id = > waiting . remove ( & s ( " id " ) ) . map ( | ( w , _ ) | w ) ,
_ = > None ,
}
} ;
let Some ( w ) = w else { return } ;
let headers = head [ " headers " ] . as_array ( ) . into_iter ( ) . flatten ( ) . filter_map ( | x | Some ( ( x [ 0 ] . as_str ( ) ? . to_string ( ) , x [ 1 ] . as_str ( ) ? . to_string ( ) ) ) ) . collect ( ) ;
let _ = w . send ( Answer { status : head [ " status " ] . as_u64 ( ) . unwrap_or ( 502 ) as u16 , ctype : s ( " ctype " ) , headers , body , z : head [ " z " ] . as_bool ( ) . unwrap_or ( false ) } ) ;
2026-11-04 11:30:32 +00:00
}
// One AnyBot asks another of the same account (大腦 sync, 裝置連結).
" call " = > {
let ( id , to , path ) = ( s ( " id " ) , s ( " to " ) , s ( " path " ) ) ;
let role = if path = = " /api/device/run " {
" device "
} else if path = = " /api/sync " | | path . starts_with ( " /api/sync/ " ) {
" mirror "
} else {
let a = Answer ::error ( 404 , " 沒有這個網址 " ) ;
let _ = tx . send ( wire ::pack ( & json! ( { " t " : " ret " , " id " : id , " status " : a . status , " ctype " : a . ctype } ) , & a . body ) ) ;
return ;
} ;
let same = self . data . lock ( ) . unwrap ( ) . hosts . iter ( ) . any ( | x | x . id = = to & & x . user = = h . user ) ;
if ! same | | to = = h . id {
let a = Answer ::error ( 403 , " 不是同一個帳號的電腦 " ) ;
let _ = tx . send ( wire ::pack ( & json! ( { " t " : " ret " , " id " : id , " status " : a . status , " ctype " : a . ctype } ) , & a . body ) ) ;
return ;
}
let query : HashMap < String , String > = head [ " query " ] . as_object ( ) . map ( | o | o . iter ( ) . map ( | ( k , v ) | ( k . clone ( ) , v . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ) ) . collect ( ) ) . unwrap_or_default ( ) ;
2026-11-04 12:38:45 +00:00
let ( me , tx , user , method , z , from ) = ( self . clone ( ) , tx . clone ( ) , h . user . clone ( ) , s ( " method " ) , head [ " z " ] . as_bool ( ) . unwrap_or ( false ) , h . id . clone ( ) ) ;
2026-11-04 11:30:32 +00:00
std ::thread ::spawn ( move | | {
2026-11-04 12:38:45 +00:00
let a = me . forward_raw ( & to , & method , & path , & query , & body , z , ( & user , role ) , true , & from ) ;
2026-11-05 14:34:36 +00:00
// The answer's headers too (a live picture's number, a screen's size).
let headers : Vec < [ & str ; 2 ] > = a . headers . iter ( ) . map ( | ( k , v ) | [ k . as_str ( ) , v . as_str ( ) ] ) . collect ( ) ;
let _ = tx . send ( wire ::pack ( & json! ( { " t " : " ret " , " id " : id , " status " : a . status , " ctype " : a . ctype , " z " : a . z , " headers " : headers } ) , & a . body ) ) ;
2026-11-04 11:30:32 +00:00
} ) ;
}
" hello " = > {
2026-11-04 12:56:06 +00:00
if head [ " hands " ] . as_bool ( ) . unwrap_or ( false ) {
self . hands . lock ( ) . unwrap ( ) . insert ( h . id . clone ( ) ) ;
} else {
self . hands . lock ( ) . unwrap ( ) . remove ( & h . id ) ;
}
2026-11-04 11:30:32 +00:00
let brain = s ( " brain " ) ;
let mut u = self . uses . lock ( ) . unwrap ( ) ;
if brain . is_empty ( ) {
u . remove ( & h . id ) ;
} else {
u . insert ( h . id . clone ( ) , brain ) ;
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-04 11:30:32 +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 handle ( & self , req : Req ) -> Resp {
let parts : Vec < String > = req . parts ( ) . iter ( ) . map ( | s | s . to_string ( ) ) . collect ( ) ;
let p : Vec < & str > = parts . iter ( ) . map ( | s | s . as_str ( ) ) . collect ( ) ;
match ( req . method . as_str ( ) , p . as_slice ( ) ) {
( " GET " , [ ] ) | ( " GET " , [ " index.html " ] ) = > Resp ::bytes ( " text/html; charset=utf-8 " , PAGE . as_bytes ( ) . to_vec ( ) )
. with_header ( " Content-Security-Policy " , " default-src 'self'; img-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' " )
. with_header ( " Strict-Transport-Security " , " max-age=31536000 " ) ,
( " GET " , [ " manifest.webmanifest " ] ) = > Resp ::bytes ( " application/manifest+json " , MANIFEST . as_bytes ( ) . to_vec ( ) ) ,
( " GET " , [ " icon.svg " ] ) = > Resp ::bytes ( " image/svg+xml " , ICON . as_bytes ( ) . to_vec ( ) ) ,
2026-11-05 11:20:29 +00:00
( " GET " , [ name ] ) if ICON_PNGS . iter ( ) . any ( | ( n , _ ) | n = = name ) = > Resp ::bytes ( " image/png " , ICON_PNGS . iter ( ) . find ( | ( n , _ ) | n = = name ) . map ( | ( _ , b ) | b . to_vec ( ) ) . unwrap_or_default ( ) ) ,
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
( " GET " , [ " api " , " hello " ] ) = > Resp ::json ( & json! ( { " app " : " anybot-relay " } ) ) ,
( " GET " , [ " sw.js " ] ) = > Resp ::bytes ( " text/javascript; charset=utf-8 " , SW . as_bytes ( ) . to_vec ( ) ) . with_header ( " Service-Worker-Allowed " , " / " ) ,
( " GET " , [ " relay " , " push " , " key " ] ) = > Resp ::json ( & json! ( { " key " : self . vapid . public_b64 } ) ) ,
( " POST " , [ " relay " , " push " , " subscribe " ] ) = > {
let Some ( user ) = self . session ( & req ) else { return Resp ::error ( 401 , " 請先登入 " ) } ;
let sub = req . json ( ) ;
let endpoint = sub [ " endpoint " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
if ! endpoint . starts_with ( " https:// " ) | | sub [ " keys " ] [ " p256dh " ] . as_str ( ) . is_none ( ) | | sub [ " keys " ] [ " auth " ] . as_str ( ) . is_none ( ) {
return Resp ::error ( 400 , " 通知訂閱資料不完整 " ) ;
}
let mut d = self . data . lock ( ) . unwrap ( ) ;
d . subs . retain ( | s | s . sub [ " endpoint " ] . as_str ( ) ! = Some ( endpoint . as_str ( ) ) ) ;
d . subs . push ( Sub { user , sub , created : now ( ) } ) ;
self . save ( & d ) ;
Resp ::json ( & json! ( { " ok " : true } ) )
}
( " POST " , [ " relay " , " push " , " unsubscribe " ] ) = > {
let endpoint = req . json ( ) [ " endpoint " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
let mut d = self . data . lock ( ) . unwrap ( ) ;
d . subs . retain ( | s | s . sub [ " endpoint " ] . as_str ( ) ! = Some ( endpoint . as_str ( ) ) ) ;
self . save ( & d ) ;
Resp ::json ( & json! ( { " ok " : true } ) )
}
// An AnyBot tells its person something (a bot needs them, or finished while they were away).
( " POST " , [ " relay " , " host " , " notify " ] ) = > {
let Some ( h ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
let v = req . json ( ) ;
let payload = json! ( { " title " : v [ " title " ] . as_str ( ) . unwrap_or ( " AnyBot " ) . chars ( ) . take ( 80 ) . collect ::< String > ( ) , " body " : v [ " body " ] . as_str ( ) . unwrap_or ( " " ) . chars ( ) . take ( 200 ) . collect ::< String > ( ) , " bot " : v [ " bot " ] , " host " : h . id } ) ;
let subs : Vec < Sub > = self . data . lock ( ) . unwrap ( ) . subs . iter ( ) . filter ( | s | matches! ( h . role_of ( & s . user ) . as_deref ( ) , Some ( " owner " ) | Some ( " admin " ) ) ) . cloned ( ) . collect ( ) ;
let mut gone = vec! [ ] ;
let mut sent = 0 ;
for s in & subs {
match push ::send ( & self . vapid , & s . sub , & payload , & self . contact ) {
Ok ( ( ) ) = > sent + = 1 ,
Err ( true ) = > gone . push ( s . sub [ " endpoint " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ) ,
Err ( false ) = > { }
}
}
if ! gone . is_empty ( ) {
let mut d = self . data . lock ( ) . unwrap ( ) ;
d . subs . retain ( | s | ! gone . iter ( ) . any ( | g | s . sub [ " endpoint " ] . as_str ( ) = = Some ( g . as_str ( ) ) ) ) ;
self . save ( & d ) ;
}
Resp ::json ( & json! ( { " sent " : sent } ) )
}
( " POST " , [ " relay " , " signup " ] ) = > self . signup ( & req ) ,
( " POST " , [ " relay " , " login " ] ) = > self . login ( & req ) ,
( " POST " , [ " relay " , " logout " ] ) = > {
let h = httpd ::sha256_hex ( & req . bearer ( ) ) ;
let mut d = self . data . lock ( ) . unwrap ( ) ;
d . sessions . retain ( | s | s . hash ! = h ) ;
self . save ( & d ) ;
Resp ::json ( & json! ( { " ok " : true } ) )
}
( " GET " , [ " relay " , " me " ] ) = > {
let Some ( user ) = self . session ( & req ) else { return Resp ::error ( 401 , " 請先登入 " ) } ;
let d = self . data . lock ( ) . unwrap ( ) ;
let hosts : Vec < Value > = d
. hosts
. iter ( )
. filter_map ( | h | h . role_of ( & user ) . map ( | r | ( h , r ) ) )
. map ( | ( h , r ) | {
let mut v = self . host_json ( h ) ;
v [ " role " ] = json! ( r ) ;
if r ! = " owner " {
v [ " name " ] = json! ( format! ( " {} ( {} 的) " , h . name , h . user ) ) ;
}
v
} )
. collect ( ) ;
let sessions : Vec < Value > = d . sessions . iter ( ) . filter ( | s | s . user = = user ) . map ( | s | json! ( { " device " : s . device , " created " : s . created , " last " : s . last } ) ) . collect ( ) ;
Resp ::json ( & json! ( { " user " : user , " hosts " : hosts , " sessions " : sessions , " backup " : self . dir . join ( " backups " ) . join ( format! ( " {} .bin " , httpd ::sha256_hex ( & user ) ) ) . exists ( ) } ) )
}
( " POST " , [ " relay " , " hosts " ] ) = > {
let Some ( user ) = self . session ( & req ) else { return Resp ::error ( 401 , " 請先登入 " ) } ;
let name : String = req . json ( ) [ " name " ] . as_str ( ) . unwrap_or ( " AnyBot " ) . chars ( ) . take ( 60 ) . collect ( ) ;
2026-11-04 12:56:06 +00:00
let ( h , token ) = self . add_host ( & user , & name ) ;
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
Resp ::json ( & json! ( { " id " : h . id , " token " : token } ) )
}
2026-11-04 12:56:06 +00:00
// 加一台電腦: a signed-in phone or AnyBot asks for a code; the new computer gives it back.
( " POST " , [ " relay " , " pair " ] ) = > {
let Some ( user ) = self . session ( & req ) . or_else ( | | self . host ( & req ) . map ( | h | h . user ) ) else { return Resp ::error ( 401 , " 請先登入 " ) } ;
let want = req . json ( ) [ " for " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
let for_host = if self . data . lock ( ) . unwrap ( ) . hosts . iter ( ) . any ( | h | h . id = = want & & h . user = = user ) { want } else { String ::new ( ) } ;
let mut pairs = self . pairs . lock ( ) . unwrap ( ) ;
pairs . retain ( | _ , p | p . 2. elapsed ( ) . as_secs ( ) < PAIR_SECS ) ;
let code = loop {
let n = u32 ::from_str_radix ( & httpd ::random_hex ( 4 ) , 16 ) . unwrap_or ( 0 ) % 1_000_000 ;
let c = format! ( " {n:06} " ) ;
if ! pairs . contains_key ( & c ) {
break c ;
}
} ;
pairs . insert ( code . clone ( ) , ( user , for_host , Instant ::now ( ) ) ) ;
Resp ::json ( & json! ( { " code " : code , " expires_in " : PAIR_SECS } ) )
}
( " POST " , [ " relay " , " pair " , " claim " ] ) = > {
let key = format! ( " pair: {} " , req . peer ) ;
if self . too_many_fails ( & key ) {
return Resp ::error ( 429 , " 配對碼錯太多次,請 5 分鐘後再試 " ) ;
}
let v = req . json ( ) ;
let code : String = v [ " code " ] . as_str ( ) . unwrap_or ( " " ) . chars ( ) . filter ( | c | c . is_ascii_digit ( ) ) . collect ( ) ;
let got = {
let mut pairs = self . pairs . lock ( ) . unwrap ( ) ;
pairs . retain ( | _ , p | p . 2. elapsed ( ) . as_secs ( ) < PAIR_SECS ) ;
pairs . remove ( & code )
} ;
let Some ( ( user , for_host , _ ) ) = got else {
self . failed ( & key ) ;
return Resp ::error ( 404 , " 配對碼不對, 或已經過期( 10 分鐘內有效,只能用一次) " ) ;
} ;
let name : String = v [ " name " ] . as_str ( ) . unwrap_or ( " AnyBot " ) . chars ( ) . take ( 60 ) . collect ( ) ;
let ( h , token ) = self . add_host ( & user , & name ) ;
let for_name = self . data . lock ( ) . unwrap ( ) . hosts . iter ( ) . find ( | x | x . id = = for_host ) . map ( | x | x . name . clone ( ) ) . unwrap_or_default ( ) ;
Resp ::json ( & json! ( { " id " : h . id , " token " : token , " user " : user , " for " : for_host , " for_name " : for_name } ) )
}
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
( " POST " , [ " relay " , " hosts " , id , " remove " ] ) = > {
let Some ( user ) = self . session ( & req ) else { return Resp ::error ( 401 , " 請先登入 " ) } ;
let mut d = self . data . lock ( ) . unwrap ( ) ;
d . hosts . retain ( | x | ! ( x . user = = user & & x . id = = * id ) ) ;
self . save ( & d ) ;
Resp ::json ( & json! ( { " ok " : true } ) )
}
( " GET " , [ " relay " , " host " , " next " ] ) = > {
let Some ( h ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
self . seen . lock ( ) . unwrap ( ) . insert ( h . id . clone ( ) , Instant ::now ( ) ) ;
let q = self . queues . lock ( ) . unwrap ( ) ;
let ( mut q , _ ) = self . arrived . wait_timeout_while ( q , Duration ::from_secs ( HOST_POLL_SECS ) , | q | q . get ( & h . id ) . map ( | x | x . is_empty ( ) ) . unwrap_or ( true ) ) . unwrap ( ) ;
self . seen . lock ( ) . unwrap ( ) . insert ( h . id . clone ( ) , Instant ::now ( ) ) ;
match q . get_mut ( & h . id ) . and_then ( | x | x . pop_front ( ) ) {
Some ( job ) = > Resp ::json ( & job ) ,
None = > Resp ::empty ( 204 ) ,
}
}
( " POST " , [ " relay " , " host " , " reply " , id ] ) = > {
2026-11-05 07:48:20 +00:00
let Some ( h ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
// Only the host the job was sent to may answer it.
let tx = {
let mut waiting = self . waiting . lock ( ) . unwrap ( ) ;
match waiting . get ( * id ) {
Some ( ( _ , target ) ) if * target = = h . id = > waiting . remove ( * id ) . map ( | ( w , _ ) | w ) ,
_ = > None ,
}
} ;
if let Some ( tx ) = tx {
2026-11-04 11:30:32 +00:00
let _ = tx . send ( Answer ::from_json ( & req . json ( ) ) ) ;
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
}
Resp ::json ( & json! ( { " ok " : true } ) )
}
( " GET " , [ " relay " , " host " , " members " ] ) = > {
let Some ( h ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
Resp ::json ( & json! ( { " members " : h . members . iter ( ) . map ( | m | json! ( { " user " : m . user , " role " : m . role } ) ) . collect ::< Vec < _ > > ( ) } ) )
}
( " POST " , [ " relay " , " host " , " members " ] ) = > {
let Some ( h ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
let v = req . json ( ) ;
let user = v [ " user " ] . as_str ( ) . unwrap_or ( " " ) . trim ( ) . to_lowercase ( ) ;
let role = v [ " role " ] . as_str ( ) . unwrap_or ( " member " ) . to_string ( ) ;
let mut d = self . data . lock ( ) . unwrap ( ) ;
if role ! = " remove " & & ! d . users . iter ( ) . any ( | u | u . name = = user ) {
return Resp ::error ( 404 , " 這台伺服器上沒有這個帳號(請對方先建立帳號) " ) ;
}
if user = = h . user {
return Resp ::error ( 400 , " 這是你自己的帳號 " ) ;
}
if ! matches! ( role . as_str ( ) , " member " | " admin " | " remove " ) {
return Resp ::error ( 400 , " 角色只能是 member 或 admin " ) ;
}
if let Some ( x ) = d . hosts . iter_mut ( ) . find ( | x | x . id = = h . id ) {
x . members . retain ( | m | m . user ! = user ) ;
if role ! = " remove " {
x . members . push ( Member { user , role } ) ;
}
}
self . save ( & d ) ;
Resp ::json ( & json! ( { " ok " : true } ) )
}
// 發布網站: an AnyBot publishes a folder; it is served at /s/<user>-<name>/.
( " POST " , [ " relay " , " host " , " sites " , name ] ) = > {
let Some ( h ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
let name = name . to_lowercase ( ) ;
if name . is_empty ( ) | | name . len ( ) > 40 | | ! name . chars ( ) . all ( | c | c . is_ascii_alphanumeric ( ) | | c = = '-' ) {
return Resp ::error ( 400 , " 網站名稱只能用英文字母、數字和 -(最多 40 個字) " ) ;
}
let site = format! ( " {} - {name} " , h . user . replace ( | c : char | ! c . is_ascii_alphanumeric ( ) , " - " ) ) ;
let dir = self . dir . join ( " sites " ) . join ( & site ) ;
let v = req . json ( ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
if v [ " remove " ] . as_bool ( ) = = Some ( true ) {
return Resp ::json ( & json! ( { " ok " : true } ) ) ;
}
let mut n = 0 ;
for f in v [ " files " ] . as_array ( ) . into_iter ( ) . flatten ( ) {
let rel = f [ " path " ] . as_str ( ) . unwrap_or ( " " ) ;
if rel . is_empty ( ) | | rel . split ( '/' ) . any ( | c | c . is_empty ( ) | | c = = " . " | | c = = " .. " ) {
continue ;
}
let Ok ( bytes ) = base64 ::Engine ::decode ( & base64 ::engine ::general_purpose ::STANDARD , f [ " data " ] . as_str ( ) . unwrap_or ( " " ) ) else { continue } ;
let path = dir . join ( rel ) ;
let _ = std ::fs ::create_dir_all ( path . parent ( ) . unwrap_or ( & dir ) ) ;
if std ::fs ::write ( & path , bytes ) . is_ok ( ) {
n + = 1 ;
}
}
let public = std ::env ::var ( " ANYBOT_RELAY_PUBLIC_URL " ) . unwrap_or_default ( ) ;
Resp ::json ( & json! ( { " files " : n , " path " : format ! ( " /s/{site}/ " ) , " url " : if public . is_empty ( ) { String ::new ( ) } else { format! ( " {} /s/ {site} / " , public . trim_end_matches ( '/' ) ) } } ) )
}
( " GET " , [ " s " , site , rest @ .. ] ) = > {
if site . contains ( " .. " ) | | rest . iter ( ) . any ( | c | c . is_empty ( ) | | * c = = " . " | | * c = = " .. " ) {
return Resp ::error ( 404 , " 找不到 " ) ;
}
let mut path = self . dir . join ( " sites " ) . join ( site ) . join ( rest . join ( " / " ) ) ;
if path . is_dir ( ) | | rest . is_empty ( ) {
path = path . join ( " index.html " ) ;
}
let ext = path . extension ( ) . map ( | e | e . to_string_lossy ( ) . to_lowercase ( ) ) . unwrap_or_default ( ) ;
let ctype = match ext . as_str ( ) {
" html " | " htm " = > " text/html; charset=utf-8 " ,
" css " = > " text/css; charset=utf-8 " ,
" js " | " mjs " = > " text/javascript; charset=utf-8 " ,
" json " = > " application/json " ,
" svg " = > " image/svg+xml " ,
" png " = > " image/png " ,
" jpg " | " jpeg " = > " image/jpeg " ,
" gif " = > " image/gif " ,
" webp " = > " image/webp " ,
" ico " = > " image/x-icon " ,
" txt " | " md " = > " text/plain; charset=utf-8 " ,
" pdf " = > " application/pdf " ,
" woff2 " = > " font/woff2 " ,
" mp4 " = > " video/mp4 " ,
_ = > " application/octet-stream " ,
} ;
match std ::fs ::read ( & path ) {
// Sandboxed: a published page runs in its own opaque origin, so it can never
// read the phone app's sign-in on this same address.
Ok ( b ) = > Resp ::bytes ( ctype , b ) . with_header ( " Content-Security-Policy " , " sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads " ) ,
Err ( _ ) = > Resp ::error ( 404 , " 找不到這個網頁 " ) ,
}
}
( " GET " , [ " relay " , " host " , " peers " ] ) = > {
let Some ( me ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
let d = self . data . lock ( ) . unwrap ( ) ;
Resp ::json ( & json! ( { " hosts " : d . hosts . iter ( ) . filter ( | h | h . user = = me . user ) . map ( | h | self . host_json ( h ) ) . collect ::< Vec < _ > > ( ) } ) )
}
2026-11-05 07:48:20 +00:00
// Signing out of the app unregisters this machine: its own key removes its own
// registration, so a key left on a borrowed computer dies with the sign-out.
( " POST " , [ " relay " , " host " , " remove " ] ) = > {
let Some ( h ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
{
let mut d = self . data . lock ( ) . unwrap ( ) ;
d . hosts . retain ( | x | x . id ! = h . id ) ;
self . save ( & d ) ;
}
// Its open connection is closed, so it hears "the key is gone" at once.
if let Some ( s ) = self . sockets . lock ( ) . unwrap ( ) . remove ( & h . id ) {
let _ = s . tx . send ( wire ::pack ( & json! ( { " t " : " bye " } ) , & [ ] ) ) ;
}
Resp ::json ( & json! ( { " ok " : true } ) )
}
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
( " PUT " , [ " relay " , " backup " ] ) = > {
let Some ( h ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
2026-11-05 07:48:20 +00:00
if req . body . len ( ) > 200 * 1024 * 1024 {
return Resp ::error ( 413 , " 備份太大了(最多 200 MB) " ) ;
}
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 path = self . dir . join ( " backups " ) . join ( format! ( " {} .bin " , httpd ::sha256_hex ( & h . user ) ) ) ;
let tmp = path . with_extension ( " tmp " ) ;
match std ::fs ::write ( & tmp , & req . body ) . and_then ( | _ | std ::fs ::rename ( & tmp , & path ) ) {
Ok ( ( ) ) = > Resp ::json ( & json! ( { " ok " : true , " bytes " : req . body . len ( ) } ) ) ,
Err ( e ) = > Resp ::error ( 500 , & e . to_string ( ) ) ,
}
}
( " GET " , [ " relay " , " backup " ] ) = > {
let Some ( user ) = self . session ( & req ) else { return Resp ::error ( 401 , " 請先登入 " ) } ;
match std ::fs ::read ( self . dir . join ( " backups " ) . join ( format! ( " {} .bin " , httpd ::sha256_hex ( & user ) ) ) ) {
Ok ( b ) = > Resp ::bytes ( " application/octet-stream " , b ) ,
Err ( _ ) = > Resp ::error ( 404 , " 還沒有備份 " ) ,
}
}
// 裝置連結: one AnyBot asks another AnyBot of the same account.
( " POST " , [ " relay " , " hosts " , target , " api " , " device " , " run " ] ) = > {
let Some ( me ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
let same = self . data . lock ( ) . unwrap ( ) . hosts . iter ( ) . any ( | h | h . id = = * target & & h . user = = me . user ) ;
if ! same | | * target = = me . id {
return Resp ::error ( 403 , " 不是同一個帳號的電腦 " ) ;
}
2026-11-04 12:38:45 +00:00
self . forward_raw ( target , " POST " , " /api/device/run " , & HashMap ::new ( ) , & req . body , false , ( & me . user , " device " ) , false , & me . id ) . into_resp ( )
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-04 09:46:43 +00:00
// 大腦: one AnyBot of the account shows (and edits) another one's bots and chats.
( m , [ " relay " , " hosts " , target , " api " , " sync " , rest @ .. ] ) = > {
let Some ( me ) = self . host ( & req ) else { return Resp ::error ( 401 , " 這台電腦的金鑰無效 " ) } ;
let same = self . data . lock ( ) . unwrap ( ) . hosts . iter ( ) . any ( | h | h . id = = * target & & h . user = = me . user ) ;
if ! same | | * target = = me . id {
return Resp ::error ( 403 , " 不是同一個帳號的電腦 " ) ;
}
let path = if rest . is_empty ( ) { " /api/sync " . to_string ( ) } else { format! ( " /api/sync/ {} " , rest . join ( " / " ) ) } ;
2026-11-04 12:38:45 +00:00
self . forward_raw ( target , m , & path , & req . query , & req . body , false , ( & me . user , " mirror " ) , false , & me . id ) . into_resp ( )
2026-11-04 09:46:43 +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
// The phone, through to one of the account's AnyBots.
( m , [ " h " , host , rest @ .. ] ) = > {
let Some ( user ) = self . session ( & req ) else { return Resp ::error ( 401 , " 請先登入 " ) } ;
let role = self . data . lock ( ) . unwrap ( ) . hosts . iter ( ) . find ( | h | h . id = = * host ) . and_then ( | h | h . role_of ( & user ) ) ;
let Some ( role ) = role else { return Resp ::error ( 404 , " 找不到這台 AnyBot " ) } ;
if rest . first ( ) ! = Some ( & " api " ) | | rest . get ( 1 ) = = Some ( & " device " ) {
return Resp ::error ( 404 , " 沒有這個網址 " ) ;
}
let mut q = req . query . clone ( ) ;
q . remove ( " t " ) ;
self . forward ( host , m , & format! ( " / {} " , rest . join ( " / " ) ) , & q , & req . body , ( & user , & role ) )
}
_ = > Resp ::error ( 404 , " 沒有這個網址 " ) ,
}
}
2026-11-04 12:56:06 +00:00
/// Register an AnyBot of `user`. The same name again replaces its old key (keeping who it's
/// shared with).
fn add_host ( & self , user : & str , name : & str ) -> ( Host , String ) {
let token = httpd ::random_hex ( 32 ) ;
let mut h = Host { id : httpd ::random_hex ( 8 ) , name : name . to_string ( ) , user : user . to_string ( ) , token_hash : httpd ::sha256_hex ( & token ) , created : now ( ) , last_seen : 0 , members : vec ! [ ] } ;
let mut d = self . data . lock ( ) . unwrap ( ) ;
if let Some ( old ) = d . hosts . iter ( ) . find ( | x | x . user = = user & & x . name = = name ) {
h . members = old . members . clone ( ) ;
}
d . hosts . retain ( | x | ! ( x . user = = user & & x . name = = name ) ) ;
d . hosts . push ( h . clone ( ) ) ;
self . save ( & d ) ;
( h , token )
}
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 host_json ( & self , h : & Host ) -> Value {
let online = self . online ( & h . id ) ;
2026-11-04 12:56:06 +00:00
json! ( { " id " : h . id , " name " : h . name , " online " : online , " uses " : self . uses . lock ( ) . unwrap ( ) . get ( & h . id ) . cloned ( ) . unwrap_or_default ( ) , " hands " : self . hands . lock ( ) . unwrap ( ) . contains ( & h . id ) , " last_seen_text " : if online { " 在線上 " . to_string ( ) } else if h . last_seen > 0 { chrono ::DateTime ::from_timestamp ( h . last_seen , 0 ) . map ( | t | t . format ( " %m/%d %H:%M UTC " ) . to_string ( ) ) . unwrap_or_default ( ) } else { " 從未連線 " . 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
}
fn signup ( & self , req : & Req ) -> Resp {
let v = req . json ( ) ;
let user = v [ " user " ] . as_str ( ) . unwrap_or ( " " ) . trim ( ) . to_lowercase ( ) ;
let pw = v [ " password " ] . as_str ( ) . unwrap_or ( " " ) ;
if user . len ( ) < 2 | | user . len ( ) > 64 | | ! user . chars ( ) . all ( | c | c . is_ascii_alphanumeric ( ) | | " ._@- " . contains ( c ) ) {
return Resp ::error ( 400 , " 帳號只能用英文字母、數字和 . _ @ -( 2– 64 個字) " ) ;
}
if pw . chars ( ) . count ( ) < 8 {
return Resp ::error ( 400 , " 密碼至少 8 個字 " ) ;
}
let mut d = self . data . lock ( ) . unwrap ( ) ;
if ! d . users . is_empty ( ) & & ( self . invite . is_empty ( ) | | v [ " invite " ] . as_str ( ) ! = Some ( self . invite . as_str ( ) ) ) {
return Resp ::error ( 403 , if self . invite . is_empty ( ) { " 這台伺服器不開放註冊 " } else { " 邀請碼不對 " } ) ;
}
if d . users . iter ( ) . any ( | u | u . name = = user ) {
return Resp ::error ( 409 , " 這個帳號已經有人用了 " ) ;
}
d . users . push ( User { name : user , pass : httpd ::hash_password ( pw ) , created : now ( ) } ) ;
self . save ( & d ) ;
Resp ::json ( & json! ( { " ok " : true } ) )
}
fn login ( & self , req : & Req ) -> Resp {
let v = req . json ( ) ;
let user = v [ " user " ] . as_str ( ) . unwrap_or ( " " ) . trim ( ) . to_lowercase ( ) ;
2026-11-05 07:48:20 +00:00
// Rate-limit by the real peer. A client writes its own first X-Forwarded-For entry, so
// taking the first value lets anyone reset the counter by changing the header. Only when
// RELAY_TRUST_PROXY=1 (behind a reverse proxy) use the proxy's appended (last) entry.
let ip = match ( std ::env ::var ( " RELAY_TRUST_PROXY " ) . is_ok ( ) , req . header ( " x-forwarded-for " ) ) {
( true , xff ) if ! xff . is_empty ( ) = > xff . rsplit ( ',' ) . next ( ) . unwrap_or ( xff ) . trim ( ) . to_string ( ) ,
_ = > req . peer . clone ( ) ,
} ;
let key = format! ( " {ip} | {user} " ) ;
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 self . too_many_fails ( & key ) {
return Resp ::error ( 429 , " 密碼錯太多次,請 5 分鐘後再試 " ) ;
}
let stored = self . data . lock ( ) . unwrap ( ) . users . iter ( ) . find ( | u | u . name = = user ) . map ( | u | u . pass . clone ( ) ) ;
let ok = stored . map ( | p | httpd ::check_password ( v [ " password " ] . as_str ( ) . unwrap_or ( " " ) , & p ) ) . unwrap_or ( false ) ;
if ! ok {
self . failed ( & key ) ;
std ::thread ::sleep ( Duration ::from_millis ( 500 ) ) ;
return Resp ::error ( 401 , " 帳號或密碼不對 " ) ;
}
let token = httpd ::random_hex ( 32 ) ;
let mut d = self . data . lock ( ) . unwrap ( ) ;
d . sessions . push ( Session { hash : httpd ::sha256_hex ( & token ) , user , device : v [ " device " ] . as_str ( ) . unwrap_or ( " " ) . chars ( ) . take ( 40 ) . collect ( ) , created : now ( ) , last : now ( ) } ) ;
let cutoff = now ( ) - SESSION_DAYS * 86_400 ;
d . sessions . retain ( | s | s . last > cutoff ) ;
self . save ( & d ) ;
Resp ::json ( & json! ( { " token " : token } ) )
}
/// Remember when hosts were last seen (for "離線(上次…)").
fn save_seen ( & self ) {
let seen : Vec < ( String , u64 ) > = self . seen . lock ( ) . unwrap ( ) . iter ( ) . map ( | ( k , v ) | ( k . clone ( ) , v . elapsed ( ) . as_secs ( ) ) ) . collect ( ) ;
let mut d = self . data . lock ( ) . unwrap ( ) ;
let mut changed = false ;
for ( id , ago ) in seen {
if let Some ( h ) = d . hosts . iter_mut ( ) . find ( | h | h . id = = id ) {
let t = now ( ) - ago as i64 ;
if t - h . last_seen > 60 {
h . last_seen = t ;
changed = true ;
}
}
}
if changed {
self . save ( & d ) ;
}
}
}
fn main ( ) {
let args : Vec < String > = std ::env ::args ( ) . collect ( ) ;
let arg = | name : & str | args . iter ( ) . position ( | a | a = = name ) . and_then ( | i | args . get ( i + 1 ) ) . cloned ( ) ;
let port : u16 = arg ( " --port " ) . and_then ( | p | p . parse ( ) . ok ( ) ) . or_else ( | | std ::env ::var ( " PORT " ) . ok ( ) . and_then ( | p | p . parse ( ) . ok ( ) ) ) . unwrap_or ( 8080 ) ;
let bind = arg ( " --bind " ) . unwrap_or_else ( | | " 0.0.0.0 " . into ( ) ) ;
let dir = PathBuf ::from ( arg ( " --data " ) . unwrap_or_else ( | | " relay-data " . into ( ) ) ) ;
2026-11-04 13:34:15 +00:00
// Forgot the password: only someone on this computer can set a new one.
if let Some ( user ) = arg ( " --reset-password " ) {
std ::process ::exit ( reset_password ( & dir , & user . trim ( ) . to_lowercase ( ) , port ) ) ;
}
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 relay = Arc ::new ( Relay ::load ( dir . clone ( ) ) ) ;
let listener = std ::net ::TcpListener ::bind ( ( bind . as_str ( ) , port ) ) . unwrap_or_else ( | e | {
eprintln! ( " 無法使用連接埠 {port} : {e} " ) ;
std ::process ::exit ( 1 )
} ) ;
eprintln! ( " AnyBot 帳號伺服器: http:// {bind} : {port} (資料在 {} ) " , dir . display ( ) ) ;
if relay . data . lock ( ) . unwrap ( ) . users . is_empty ( ) {
eprintln! ( " 還沒有帳號:第一個註冊的人會成為這台伺服器的使用者。 " ) ;
}
let r2 = relay . clone ( ) ;
std ::thread ::spawn ( move | | loop {
std ::thread ::sleep ( Duration ::from_secs ( 60 ) ) ;
r2 . save_seen ( ) ;
} ) ;
2026-11-04 11:30:32 +00:00
let ( r , w ) = ( relay . clone ( ) , relay . clone ( ) ) ;
httpd ::serve_upgrade ( listener , Arc ::new ( move | req | r . handle ( req ) ) , Some ( Arc ::new ( move | req , c | w . ws ( req , c ) ) ) , Arc ::new ( | | true ) ) ;
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-04 13:34:15 +00:00
/// `anybot-relay --data DIR --reset-password NAME`: a new password for an account (asked twice,
/// not shown). The server must be stopped first, or it would write its own copy back.
fn reset_password ( dir : & std ::path ::Path , user : & str , port : u16 ) -> i32 {
if std ::net ::TcpStream ::connect_timeout ( & std ::net ::SocketAddr ::from ( ( [ 127 , 0 , 0 , 1 ] , port ) ) , Duration ::from_millis ( 500 ) ) . is_ok ( ) {
eprintln! ( " 帳號伺服器還在執行(埠 {port} )。先停掉它再重設密碼,之後再啟動。 " ) ;
return 1 ;
}
let relay = Relay ::load ( dir . to_path_buf ( ) ) ;
if ! relay . data . lock ( ) . unwrap ( ) . users . iter ( ) . any ( | u | u . name = = user ) {
eprintln! ( " {} 裡沒有帳號「 {user} 」。 " , dir . display ( ) ) ;
return 1 ;
}
let ask = | prompt : & str | -> String {
use std ::io ::Write ;
eprint! ( " {prompt} " ) ;
let _ = std ::io ::stderr ( ) . flush ( ) ;
let hide = std ::process ::Command ::new ( " stty " ) . arg ( " -echo " ) . stdin ( std ::process ::Stdio ::inherit ( ) ) . stderr ( std ::process ::Stdio ::null ( ) ) . status ( ) . map ( | s | s . success ( ) ) . unwrap_or ( false ) ;
let mut s = String ::new ( ) ;
let _ = std ::io ::stdin ( ) . read_line ( & mut s ) ;
if hide {
let _ = std ::process ::Command ::new ( " stty " ) . arg ( " echo " ) . stdin ( std ::process ::Stdio ::inherit ( ) ) . stderr ( std ::process ::Stdio ::null ( ) ) . status ( ) ;
}
eprintln! ( ) ;
s . trim_end_matches ( [ '\n' , '\r' ] ) . to_string ( )
} ;
let pw = ask ( & format! ( " 「 {user} 」的新密碼(至少 8 個字): " ) ) ;
if pw . chars ( ) . count ( ) < 8 {
eprintln! ( " 密碼至少 8 個字,沒有改。 " ) ;
return 1 ;
}
if ask ( " 再輸入一次: " ) ! = pw {
eprintln! ( " 兩次不一樣,沒有改。 " ) ;
return 1 ;
}
let mut d = relay . data . lock ( ) . unwrap ( ) ;
if let Some ( u ) = d . users . iter_mut ( ) . find ( | u | u . name = = user ) {
u . pass = httpd ::hash_password ( & pw ) ;
}
// Phones signed in with the old password sign in again.
d . sessions . retain ( | s | s . user ! = user ) ;
relay . save ( & d ) ;
eprintln! ( " 已改好「 {user} 」的密碼。啟動帳號伺服器後,用新密碼登入。 " ) ;
eprintln! ( " (帳號備份是用舊密碼加密的,之後要先在電腦上按一次「立刻備份」。) " ) ;
0
}
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
#[ cfg(test) ]
mod tests {
use super ::* ;
fn post ( port : u16 , path : & str , token : & str , body : Value ) -> ( u16 , Value ) {
let agent : ureq ::Agent = ureq ::Agent ::config_builder ( ) . http_status_as_error ( false ) . build ( ) . into ( ) ;
let mut r = agent . post ( & format! ( " http://127.0.0.1: {port} {path} " ) ) . header ( " Authorization " , & format! ( " Bearer {token} " ) ) . send ( body . to_string ( ) . as_bytes ( ) ) . unwrap ( ) ;
let st = r . status ( ) . as_u16 ( ) ;
( st , serde_json ::from_str ( & r . body_mut ( ) . read_to_string ( ) . unwrap_or_default ( ) ) . unwrap_or ( Value ::Null ) )
}
fn get ( port : u16 , path : & str , token : & str ) -> ( u16 , Vec < u8 > ) {
let agent : ureq ::Agent = ureq ::Agent ::config_builder ( ) . http_status_as_error ( false ) . build ( ) . into ( ) ;
let mut r = agent . get ( & format! ( " http://127.0.0.1: {port} {path} " ) ) . header ( " Authorization " , & format! ( " Bearer {token} " ) ) . call ( ) . unwrap ( ) ;
( r . status ( ) . as_u16 ( ) , r . body_mut ( ) . read_to_vec ( ) . unwrap_or_default ( ) )
}
#[ test ]
fn account_host_and_phone_round_trip ( ) {
let dir = std ::env ::temp_dir ( ) . join ( format! ( " anybot-relay-test- {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
let relay = Arc ::new ( Relay ::load ( dir . clone ( ) ) ) ;
let l = std ::net ::TcpListener ::bind ( " 127.0.0.1:0 " ) . unwrap ( ) ;
let port = l . local_addr ( ) . unwrap ( ) . port ( ) ;
let r = relay . clone ( ) ;
std ::thread ::spawn ( move | | httpd ::serve ( l , Arc ::new ( move | req | r . handle ( req ) ) , Arc ::new ( | | true ) ) ) ;
assert_eq! ( post ( port , " /relay/signup " , " " , json! ( { " user " : " Daniel " , " password " : " 12345678 " } ) ) . 0 , 200 ) ;
// Second sign-up without an invite: refused.
assert_eq! ( post ( port , " /relay/signup " , " " , json! ( { " user " : " eve " , " password " : " 12345678 " } ) ) . 0 , 403 ) ;
assert_eq! ( post ( port , " /relay/login " , " " , json! ( { " user " : " daniel " , " password " : " wrong-pw " } ) ) . 0 , 401 ) ;
let ( _ , v ) = post ( port , " /relay/login " , " " , json! ( { " user " : " daniel " , " password " : " 12345678 " , " device " : " Mac " } ) ) ;
let session = v [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let ( _ , h ) = post ( port , " /relay/hosts " , & session , json! ( { " name " : " 工作室 Mac " } ) ) ;
let ( host_id , host_token ) = ( h [ " id " ] . as_str ( ) . unwrap ( ) . to_string ( ) , h [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ) ;
// Offline host: the phone is told so.
let ( st , _ ) = get ( port , & format! ( " /h/ {host_id} /api/state " ) , & session ) ;
assert_eq! ( st , 503 ) ;
// The "AnyBot": answers whatever comes in with the path it got.
let ht = host_token . clone ( ) ;
std ::thread ::spawn ( move | | loop {
let ( st , body ) = get ( port , " /relay/host/next " , & ht ) ;
if st = = 200 {
let job : Value = serde_json ::from_slice ( & body ) . unwrap ( ) ;
let answer = json! ( { " status " : 200 , " ctype " : " application/json " , " body " : base64 ::Engine ::encode ( & base64 ::engine ::general_purpose ::STANDARD , json! ( { " path " : job [ " path " ] , " q " : job [ " query " ] } ) . to_string ( ) ) , " headers " : [ [ " X-Size " , " 10x20 " ] , [ " Set-Cookie " , " no " ] ] } ) ;
post ( port , & format! ( " /relay/host/reply/ {} " , job [ " id " ] . as_str ( ) . unwrap ( ) ) , & ht , answer ) ;
}
} ) ;
std ::thread ::sleep ( Duration ::from_millis ( 300 ) ) ;
let agent : ureq ::Agent = ureq ::Agent ::config_builder ( ) . http_status_as_error ( false ) . build ( ) . into ( ) ;
let mut resp = agent . get ( & format! ( " http://127.0.0.1: {port} /h/ {host_id} /api/state?since=3 " ) ) . header ( " Authorization " , & format! ( " Bearer {session} " ) ) . call ( ) . unwrap ( ) ;
assert_eq! ( resp . status ( ) . as_u16 ( ) , 200 ) ;
assert_eq! ( resp . headers ( ) . get ( " x-size " ) . unwrap ( ) , " 10x20 " ) ;
assert! ( resp . headers ( ) . get ( " set-cookie " ) . is_none ( ) ) ;
let v : Value = serde_json ::from_str ( & resp . body_mut ( ) . read_to_string ( ) . unwrap ( ) ) . unwrap ( ) ;
assert_eq! ( v [ " path " ] , " /api/state " ) ;
assert_eq! ( v [ " q " ] [ " since " ] , " 3 " ) ;
// The device endpoint is only for the account's other AnyBots, never the phone.
assert_eq! ( get ( port , & format! ( " /h/ {host_id} /api/device/run " ) , & session ) . 0 , 404 ) ;
// Without a session: refused.
assert_eq! ( get ( port , & format! ( " /h/ {host_id} /api/state " ) , " nope " ) . 0 , 401 ) ;
// Backup: the host uploads, the person downloads after signing in.
let agent2 : ureq ::Agent = ureq ::Agent ::config_builder ( ) . http_status_as_error ( false ) . build ( ) . into ( ) ;
let st = agent2 . put ( & format! ( " http://127.0.0.1: {port} /relay/backup " ) ) . header ( " Authorization " , & format! ( " Bearer {host_token} " ) ) . send ( & [ 1 u8 , 2 , 3 ] [ .. ] ) . unwrap ( ) . status ( ) . as_u16 ( ) ;
assert_eq! ( st , 200 ) ;
assert_eq! ( get ( port , " /relay/backup " , & session ) . 1 , vec! [ 1 , 2 , 3 ] ) ;
let ( _ , me ) = get ( port , " /relay/me " , & session ) ;
let me : Value = serde_json ::from_slice ( & me ) . unwrap ( ) ;
assert_eq! ( me [ " hosts " ] [ 0 ] [ " online " ] , true ) ;
// Data survives a restart; tokens are stored hashed.
let saved = std ::fs ::read_to_string ( dir . join ( " data.json " ) ) . unwrap ( ) ;
assert! ( ! saved . contains ( & session ) & & ! saved . contains ( & host_token ) & & ! saved . contains ( " 12345678 " ) ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
}
#[ test ]
fn team_members_get_their_role ( ) {
let dir = std ::env ::temp_dir ( ) . join ( format! ( " anybot-relay-team- {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
let mut relay = Relay ::load ( dir . clone ( ) ) ;
relay . invite = " join-us " . into ( ) ;
let relay = Arc ::new ( relay ) ;
let l = std ::net ::TcpListener ::bind ( " 127.0.0.1:0 " ) . unwrap ( ) ;
let port = l . local_addr ( ) . unwrap ( ) . port ( ) ;
let r = relay . clone ( ) ;
std ::thread ::spawn ( move | | httpd ::serve ( l , Arc ::new ( move | req | r . handle ( req ) ) , Arc ::new ( | | true ) ) ) ;
post ( port , " /relay/signup " , " " , json! ( { " user " : " boss " , " password " : " 12345678 " } ) ) ;
assert_eq! ( post ( port , " /relay/signup " , " " , json! ( { " user " : " amy " , " password " : " 12345678 " , " invite " : " nope " } ) ) . 0 , 403 ) ;
assert_eq! ( post ( port , " /relay/signup " , " " , json! ( { " user " : " amy " , " password " : " 12345678 " , " invite " : " join-us " } ) ) . 0 , 200 ) ;
let boss = post ( port , " /relay/login " , " " , json! ( { " user " : " boss " , " password " : " 12345678 " } ) ) . 1 [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let amy = post ( port , " /relay/login " , " " , json! ( { " user " : " amy " , " password " : " 12345678 " } ) ) . 1 [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let h = post ( port , " /relay/hosts " , & boss , json! ( { " name " : " 辦公室 " } ) ) . 1 ;
let ( host_id , host_token ) = ( h [ " id " ] . as_str ( ) . unwrap ( ) . to_string ( ) , h [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ) ;
// Not shared yet: Amy can't see it.
assert_eq! ( get ( port , & format! ( " /h/ {host_id} /api/state " ) , & amy ) . 0 , 404 ) ;
assert_eq! ( post ( port , " /relay/host/members " , & host_token , json! ( { " user " : " ghost " , " role " : " member " } ) ) . 0 , 404 ) ;
assert_eq! ( post ( port , " /relay/host/members " , & host_token , json! ( { " user " : " amy " , " role " : " member " } ) ) . 0 , 200 ) ;
let me : Value = serde_json ::from_slice ( & get ( port , " /relay/me " , & amy ) . 1 ) . unwrap ( ) ;
assert_eq! ( me [ " hosts " ] [ 0 ] [ " role " ] , " member " ) ;
// The host hears who asked and in which role.
let ht = host_token . clone ( ) ;
std ::thread ::spawn ( move | | loop {
let ( st , body ) = get ( port , " /relay/host/next " , & ht ) ;
if st = = 200 {
let job : Value = serde_json ::from_slice ( & body ) . unwrap ( ) ;
let answer = json! ( { " status " : 200 , " ctype " : " application/json " , " body " : base64 ::Engine ::encode ( & base64 ::engine ::general_purpose ::STANDARD , json! ( { " user " : job [ " user " ] , " role " : job [ " role " ] } ) . to_string ( ) ) , " headers " : [ ] } ) ;
post ( port , & format! ( " /relay/host/reply/ {} " , job [ " id " ] . as_str ( ) . unwrap ( ) ) , & ht , answer ) ;
}
} ) ;
std ::thread ::sleep ( Duration ::from_millis ( 300 ) ) ;
let v : Value = serde_json ::from_slice ( & get ( port , & format! ( " /h/ {host_id} /api/state " ) , & amy ) . 1 ) . unwrap ( ) ;
assert_eq! ( ( v [ " user " ] . as_str ( ) , v [ " role " ] . as_str ( ) ) , ( Some ( " amy " ) , Some ( " member " ) ) ) ;
let v : Value = serde_json ::from_slice ( & get ( port , & format! ( " /h/ {host_id} /api/state " ) , & boss ) . 1 ) . unwrap ( ) ;
assert_eq! ( v [ " role " ] , " owner " ) ;
// Removing ends access.
post ( port , " /relay/host/members " , & host_token , json! ( { " user " : " amy " , " role " : " remove " } ) ) ;
assert_eq! ( get ( port , & format! ( " /h/ {host_id} /api/state " ) , & amy ) . 0 , 404 ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
}
#[ test ]
fn published_sites_are_sandboxed ( ) {
let dir = std ::env ::temp_dir ( ) . join ( format! ( " anybot-relay-sites- {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
let relay = Arc ::new ( Relay ::load ( dir . clone ( ) ) ) ;
let l = std ::net ::TcpListener ::bind ( " 127.0.0.1:0 " ) . unwrap ( ) ;
let port = l . local_addr ( ) . unwrap ( ) . port ( ) ;
let r = relay . clone ( ) ;
std ::thread ::spawn ( move | | httpd ::serve ( l , Arc ::new ( move | req | r . handle ( req ) ) , Arc ::new ( | | true ) ) ) ;
post ( port , " /relay/signup " , " " , json! ( { " user " : " boss " , " password " : " 12345678 " } ) ) ;
let s = post ( port , " /relay/login " , " " , json! ( { " user " : " boss " , " password " : " 12345678 " } ) ) . 1 [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let ht = post ( port , " /relay/hosts " , & s , json! ( { " name " : " m " } ) ) . 1 [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let b = | t : & str | base64 ::Engine ::encode ( & base64 ::engine ::general_purpose ::STANDARD , t ) ;
let ( st , v ) = post ( port , " /relay/host/sites/trip " , & ht , json! ( { " files " : [ { " path " : " index.html " , " data " : b ( " <h1>台南</h1> " ) } , { " path " : " css/a.css " , " data " : b ( " h1{} " ) } , { " path " : " ../evil " , " data " : b ( " x " ) } ] } ) ) ;
assert_eq! ( ( st , v [ " files " ] . as_u64 ( ) ) , ( 200 , Some ( 2 ) ) ) ;
assert_eq! ( v [ " path " ] , " /s/boss-trip/ " ) ;
let agent : ureq ::Agent = ureq ::Agent ::config_builder ( ) . http_status_as_error ( false ) . build ( ) . into ( ) ;
let mut r = agent . get ( & format! ( " http://127.0.0.1: {port} /s/boss-trip/ " ) ) . call ( ) . unwrap ( ) ;
assert! ( r . headers ( ) . get ( " content-security-policy " ) . unwrap ( ) . to_str ( ) . unwrap ( ) . starts_with ( " sandbox " ) ) ;
assert_eq! ( r . body_mut ( ) . read_to_string ( ) . unwrap ( ) , " <h1>台南</h1> " ) ;
assert_eq! ( get ( port , " /s/boss-trip/css/a.css " , " " ) . 0 , 200 ) ;
assert_eq! ( get ( port , " /s/boss-trip/../../data.json " , " " ) . 0 , 404 ) ;
assert! ( ! dir . join ( " evil " ) . exists ( ) & & ! dir . join ( " sites " ) . join ( " evil " ) . exists ( ) ) ;
assert_eq! ( post ( port , " /relay/host/sites/Bad_Name " , & ht , json! ( { } ) ) . 0 , 400 ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
}
2026-11-04 12:56:06 +00:00
#[ test ]
fn a_pairing_code_adds_a_computer_once ( ) {
let dir = std ::env ::temp_dir ( ) . join ( format! ( " anybot-relay-pair- {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
let relay = Arc ::new ( Relay ::load ( dir . clone ( ) ) ) ;
let l = std ::net ::TcpListener ::bind ( " 127.0.0.1:0 " ) . unwrap ( ) ;
let port = l . local_addr ( ) . unwrap ( ) . port ( ) ;
let r = relay . clone ( ) ;
std ::thread ::spawn ( move | | httpd ::serve ( l , Arc ::new ( move | req | r . handle ( req ) ) , Arc ::new ( | | true ) ) ) ;
post ( port , " /relay/signup " , " " , json! ( { " user " : " amy " , " password " : " 12345678 " } ) ) ;
let ( _ , v ) = post ( port , " /relay/login " , " " , json! ( { " user " : " amy " , " password " : " 12345678 " , " device " : " phone " } ) ) ;
let session = v [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let ( _ , brain ) = post ( port , " /relay/hosts " , & session , json! ( { " name " : " 雲端大腦 " } ) ) ;
// Asked from the phone, for the brain.
let ( st , p ) = post ( port , " /relay/pair " , & session , json! ( { " for " : brain [ " id " ] } ) ) ;
assert_eq! ( st , 200 ) ;
let code = p [ " code " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
assert_eq! ( code . len ( ) , 6 ) ;
// Without signing in: no code.
assert_eq! ( post ( port , " /relay/pair " , " " , json! ( { } ) ) . 0 , 401 ) ;
let ( st , h ) = post ( port , " /relay/pair/claim " , " " , json! ( { " code " : format ! ( " {} {} " , & code [ .. 3 ] , & code [ 3 .. ] ) , " name " : " 書房電腦 " } ) ) ;
assert_eq! ( st , 200 , " {h} " ) ;
assert_eq! ( h [ " user " ] , " amy " ) ;
assert_eq! ( h [ " for_name " ] , " 雲端大腦 " ) ;
// The new computer's key works.
let ( st , peers ) = get ( port , " /relay/host/peers " , h [ " token " ] . as_str ( ) . unwrap ( ) ) ;
assert_eq! ( st , 200 ) ;
assert! ( String ::from_utf8_lossy ( & peers ) . contains ( " 雲端大腦 " ) ) ;
// Once only; a wrong code is refused, and guessing is stopped.
assert_eq! ( post ( port , " /relay/pair/claim " , " " , json! ( { " code " : code , " name " : " x " } ) ) . 0 , 404 ) ;
for _ in 0 .. 10 {
post ( port , " /relay/pair/claim " , " " , json! ( { " code " : " 000000 " , " name " : " x " } ) ) ;
}
assert_eq! ( post ( port , " /relay/pair/claim " , " " , json! ( { " code " : " 000000 " , " name " : " x " } ) ) . 0 , 429 ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
}
2026-11-05 07:48:20 +00:00
#[ test ]
fn brain_mirror_link_answers_only_from_the_right_machine ( ) {
// One account with a brain, a hands machine, and a nosy third machine: the sync
// channel (brain → relay → hands → relay → brain) must carry the right roles, must
// take answers only from the machine a job went to, and a signing-out machine's key
// must die with its registration.
let dir = std ::env ::temp_dir ( ) . join ( format! ( " anybot-relay-brain- {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
let relay = Arc ::new ( Relay ::load ( dir . clone ( ) ) ) ;
let l = std ::net ::TcpListener ::bind ( " 127.0.0.1:0 " ) . unwrap ( ) ;
let port = l . local_addr ( ) . unwrap ( ) . port ( ) ;
let r = relay . clone ( ) ;
std ::thread ::spawn ( move | | httpd ::serve ( l , Arc ::new ( move | req | r . handle ( req ) ) , Arc ::new ( | | true ) ) ) ;
post ( port , " /relay/signup " , " " , json! ( { " user " : " boss " , " password " : " 12345678 " } ) ) ;
let s = post ( port , " /relay/login " , " " , json! ( { " user " : " boss " , " password " : " 12345678 " , " device " : " phone " } ) ) . 1 [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let hb = post ( port , " /relay/hosts " , & s , json! ( { " name " : " 大腦 " } ) ) . 1 ;
let btoken = hb [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let hh = post ( port , " /relay/hosts " , & s , json! ( { " name " : " 手腳 " } ) ) . 1 ;
let ( hid , htoken ) = ( hh [ " id " ] . as_str ( ) . unwrap ( ) . to_string ( ) , hh [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ) ;
let hm = post ( port , " /relay/hosts " , & s , json! ( { " name " : " 旁聽 " } ) ) . 1 ;
let mtoken = hm [ " token " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
// The hands machine: takes its jobs, lets the snooper see the id, answers a beat later.
let ( tx , rx ) = channel ::< String > ( ) ;
let ht = htoken . clone ( ) ;
std ::thread ::spawn ( move | | loop {
let ( st , body ) = get ( port , " /relay/host/next " , & ht ) ;
if st = = 200 {
let job : Value = serde_json ::from_slice ( & body ) . unwrap ( ) ;
let id = job [ " id " ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let _ = tx . send ( id . clone ( ) ) ;
std ::thread ::sleep ( Duration ::from_millis ( 400 ) ) ;
let answer = json! ( { " status " : 200 , " ctype " : " application/json " , " body " : base64 ::Engine ::encode ( & base64 ::engine ::general_purpose ::STANDARD , json! ( { " from " : " hands " , " role " : job [ " role " ] , " path " : job [ " path " ] } ) . to_string ( ) ) , " headers " : [ ] } ) ;
post ( port , & format! ( " /relay/host/reply/ {id} " ) , & ht , answer ) ;
}
} ) ;
// The snooper answers with a lie as soon as it learns a job id.
let sn = mtoken . clone ( ) ;
std ::thread ::spawn ( move | | {
let id = rx . recv ( ) . unwrap ( ) ;
let lie = json! ( { " status " : 200 , " ctype " : " application/json " , " body " : base64 ::Engine ::encode ( & base64 ::engine ::general_purpose ::STANDARD , json! ( { " from " : " snooper " } ) . to_string ( ) ) , " headers " : [ ] } ) ;
post ( port , & format! ( " /relay/host/reply/ {id} " ) , & sn , lie ) ;
} ) ;
std ::thread ::sleep ( Duration ::from_millis ( 200 ) ) ;
// 大腦 syncs 手腳's chats: the answer must come from the hands, with the mirror role.
let v : Value = serde_json ::from_slice ( & get ( port , & format! ( " /relay/hosts/ {hid} /api/sync?since=0 " ) , & btoken ) . 1 ) . unwrap ( ) ;
assert_eq! ( ( v [ " from " ] . as_str ( ) , v [ " role " ] . as_str ( ) , v [ " path " ] . as_str ( ) ) , ( Some ( " hands " ) , Some ( " mirror " ) , Some ( " /api/sync " ) ) , " the brain's sync answer must be the hands' own " ) ;
// 裝置連結: running a tool on the other machine carries the device role.
let v : Value = post ( port , & format! ( " /relay/hosts/ {hid} /api/device/run " ) , & btoken , json! ( { " tool " : " shell " , " args " : " {} " , " who " : " 大腦 " } ) ) . 1 ;
assert_eq! ( ( v [ " from " ] . as_str ( ) , v [ " role " ] . as_str ( ) ) , ( Some ( " hands " ) , Some ( " device " ) ) ) ;
// Signing out: the machine's own key removes its registration; others can't reach it.
assert_eq! ( post ( port , " /relay/host/remove " , & mtoken , json! ( { } ) ) . 0 , 200 ) ;
let hosts : Value = serde_json ::from_slice ( & get ( port , " /relay/host/peers " , & btoken ) . 1 ) . unwrap ( ) ;
assert_eq! ( hosts [ " hosts " ] . as_array ( ) . map ( | a | a . len ( ) ) , Some ( 2 ) ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
}
#[ test ]
fn forged_forwarded_for_cannot_outrun_the_login_lock ( ) {
// The login throttle follows the real peer: rewriting the first X-Forwarded-For
// entry on every try used to reset the counter, so passwords could be guessed forever.
let dir = std ::env ::temp_dir ( ) . join ( format! ( " anybot-relay-throttle- {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
let relay = Arc ::new ( Relay ::load ( dir . clone ( ) ) ) ;
let l = std ::net ::TcpListener ::bind ( " 127.0.0.1:0 " ) . unwrap ( ) ;
let port = l . local_addr ( ) . unwrap ( ) . port ( ) ;
let r = relay . clone ( ) ;
std ::thread ::spawn ( move | | httpd ::serve ( l , Arc ::new ( move | req | r . handle ( req ) ) , Arc ::new ( | | true ) ) ) ;
post ( port , " /relay/signup " , " " , json! ( { " user " : " boss " , " password " : " 12345678 " } ) ) ;
let agent : ureq ::Agent = ureq ::Agent ::config_builder ( ) . http_status_as_error ( false ) . build ( ) . into ( ) ;
for i in 0 .. 8 {
let mut rr = agent
. post ( & format! ( " http://127.0.0.1: {port} /relay/login " ) )
. header ( " X-Forwarded-For " , & format! ( " 10.9. {i} . {i} " ) )
. send ( json! ( { " user " : " boss " , " password " : " wrong-password " } ) . to_string ( ) . as_bytes ( ) )
. unwrap ( ) ;
let _ = rr . body_mut ( ) . read_to_string ( ) ;
}
// Every fake IP was new; the one real peer is locked out — even for the right password.
let rr = agent
. post ( & format! ( " http://127.0.0.1: {port} /relay/login " ) )
. header ( " X-Forwarded-For " , " 10.9.99.99 " )
. send ( json! ( { " user " : " boss " , " password " : " 12345678 " } ) . to_string ( ) . as_bytes ( ) )
. unwrap ( ) ;
assert_eq! ( rr . status ( ) . as_u16 ( ) , 429 ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
}
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
}