592 lines
50 KiB
Markdown
592 lines
50 KiB
Markdown
# How the original works (and what this rebuild copies)
|
||
|
||
Original: Grok Bot 0.78.1, an Electron app.
|
||
|
||
## The native main program (decompiled)
|
||
`Contents/MacOS/Grok Bot` (Mach-O arm64, 53 040 bytes, sha256 df5b07de…). Opened with
|
||
core_target_open, and every function read with native_function_pseudocode:
|
||
|
||
| Address | Function | What it does |
|
||
|---|---|---|
|
||
| `0x100000460` | `entry` | Calls `0x1000004c0`, then `electron::fuses::IsRunAsNodeEnabled()`. If the fuse is on and `getenv("ELECTRON_RUN_AS_NODE")` is a non-empty string, it runs `ElectronInitializeICUandStartNode`, otherwise `ElectronMain`. |
|
||
| `0x1000004c0` | `sub_1000004c0` | For stdin/stdout/stderr (fd 0, 1, 2): if `fstat` fails with `errno == 9` (EBADF), reopens that stream on `/dev/null`. |
|
||
| `0x1000005bc`–`0x100000604` | import stubs | `ElectronInitializeICUandStartNode`, `ElectronMain`, `getenv`, `IsRunAsNodeEnabled`, `freopen`, `__error`, `fstat`. |
|
||
|
||
These are all 10 functions in the file (evidence `ev_407aa9c3…`). `native_strings_find` for
|
||
`model|grok|provider|chat|bot|theme|api` finds **0 strings** (evidence `ev_1b74b817…`), so none of the
|
||
chat, model or settings behaviour is native code. It lives in the JavaScript inside
|
||
`Contents/Resources/app.asar`. The sources below therefore give **file + byte offset (@N) + minified
|
||
function name**, the JavaScript equivalent of a function address.
|
||
Pseudocode evidence: `ev_7c151be5…` (entry), `ev_29ffc0d8…` (sub_1000004c0).
|
||
Bundle map evidence: `ev_2bb06b4aef290dafe0a74085416f36482d30fc7ecc476b97f343e324ceb9c9f7` (js_app_map).
|
||
|
||
## Errors, retries and timeouts
|
||
From `dist/electron-main/main-core.cjs`.
|
||
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| Retry attempts | 3 | @1082927 `D3={name:"gateway-create-agent-retry",maxAttempts:3,…}` |
|
||
| Retry delay | starts at 1000 ms, ×2 each time, at most 4000 ms → 1 s, 2 s, 4 s | same object: `initialDelayMs:1e3,maxDelayMs:4e3,backoffFactor:2` |
|
||
| Connect deadline | 15 000 ms | @1082927 `$D=15e3` → `connectDeadline` "gateway-sse-connect" (@1084759 `L3`) |
|
||
| Stream silence counts as a stall | 35 000 ms | @1082927 `VD=35e3` → `stallWatchdog` "gateway-sse-stall", `idleMs:VD` (@1084759 `L3`) |
|
||
| Provider overloaded / rate limit | retry | @689060 `SAND-E0401 providerOverloaded, retryable:!0` |
|
||
| Nothing streamed by the first-token deadline | retry | @689284 `SAND-E0402 firstTokenStall, retryable:!0`. The deadline itself is server-side: **estimated 120 s** |
|
||
| Transport reset | retry | @689501 `SAND-E0403 streamReset, retryable:!0` |
|
||
| Context window overflow | no retry | @689722 `SAND-E0404 contextWindowOverflow, retryable:!1` |
|
||
| Backend rejected | no retry | @689948 `SAND-E0405 backendRejected, retryable:!1` |
|
||
| Server-paced retry-after | wait the given delay, then retry | @690623 `SAND-E0408 backendCapacityDeferred` (30 s cap: **estimated**) |
|
||
| Silence after output started | cancel; resumed only from a checkpoint | @691348 `SAND-E0415 streamIdleStall`, so the rebuild never auto-retries once text has appeared |
|
||
| Backend unreachable (refused / DNS) | retry | @692409 `SAND-E0418 turnBackendUnreachable, retryable:!0` |
|
||
| Rate-limit banner | "Rate limited by model provider, retrying…" | `main-app.cjs` `rate_limited_reconnect_message_config` |
|
||
| Error texts | provider_overloaded → `wbQwKx`; first_token_stall → `Ly7/5m` "模型未能及時開始回應。請重試。" | `index.eager-app` @1592250, map `YN` |
|
||
|
||
How the rebuild applies them: 429, 408, 5xx, 529 and connection failures retry up to 3 tries, waiting
|
||
1 s, then 2 s, then 4 s (or the server's Retry-After). The reply shows "…秒後重試(第 n 次,共 3 次)"
|
||
meanwhile. A 400 that says the prompt is too long stops with a "對話太長了" message, and 401/403/404
|
||
stop at once. A reply that goes 35 s silent after it started is stopped, and the partial text is kept.
|
||
|
||
## Models
|
||
| What | Value | Source |
|
||
|---|---|---|
|
||
| Default model | `grok-4.5` | `main-core.cjs`, `var Wp="grok-4.5"`, exported as `SAND_DEFAULT_MODEL_ID` |
|
||
| Model choice shape | `{modelId, maxMode, parameters:[{id,value}]}` | `main-core.cjs`, schema `UEe` / `bh` |
|
||
| Model list | fetched from the server, not stored in the app | `model_picker_experiments` in `main-app.cjs` names only `grok-4.5` and `grok-4.6` |
|
||
| Setting text | "模型:選擇 Grok Bot 用來思考的模型" | `index.eager-app` settings map `b$.model` → ids `scu3wk` / `2RPCms` |
|
||
| Overload message | "模型供應商已超載" / "模型供應商目前負載過高。通常是暫時的。" | zh-TW catalog ids `Rdu4z7`, `wbQwKx` |
|
||
|
||
The original only talks to its own cloud service. **The rebuild's change**, as the person asked:
|
||
every bot picks its own provider (xAI Grok, OpenAI, Claude, Gemini, OpenRouter, Ollama, or any
|
||
OpenAI-compatible service) and model. Keys live in Settings → AI 供應商.
|
||
|
||
## Message box
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| Send | Enter sends; Shift+Enter adds a new line; Enter is ignored while an input method is composing | `index.eager-app` @2002346 `mwt`: `e.key==="Enter"&&!e.shiftKey&&!e.nativeEvent.isComposing&&e.preventDefault()` |
|
||
| Placeholder | "傳訊息給 {name}" (fallback "想問什麼都可以,或拖入檔案") | `index.eager-app` function `E6t`, ids `pqr+oY` / `HOCPPm` |
|
||
|
||
## Sidebar
|
||
From `dist/renderer/assets/index.eager-app-D1juOWZp.js`.
|
||
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| Default width | 280 px | @912025 `FT={expandedWidth:280,isCollapsed:!1}` |
|
||
| Width limits | 240–400 px | @914394 `sKe`: `jT(UT(t.expandedWidth,280),240,400)`; @969523 `QJe`: `Math.max(240,Math.min(400,…))` |
|
||
| Folded width | 88 px rail | @969523 `fD=88` |
|
||
| Fold when dragged below | 210 px | @969523 `JJe=210` |
|
||
| Unfold from the rail when dragged to | 220 px | @969523 `YJe=220` |
|
||
| Saved keys | `sand.sidebar.width`, `sand.sidebar.collapsed` | @912025 `BT` |
|
||
| Chat minimum width | 424 px | index CSS `--sand-chat-min-width:424px` (class sand-1fiktjy); window minimum = 240 + 424 |
|
||
| Sections | 主要 Bot, 已釘選的 Bot, Bot | zh-TW ids `V8a0q9`, `vPVg0Q`, `ixkTse` |
|
||
| Search | "搜尋 Bot"; empty result "沒有相符的 Bot" | ids `YjO4Og`, `12i8o8` |
|
||
|
||
## Chat
|
||
| What | Value | Source |
|
||
|---|---|---|
|
||
| Bubble corner | 18 px | index CSS `--sand-transcript-prose-bubble-radius:18px` |
|
||
| Bubble text | 15 px on a 22 px line | `--sand-transcript-prose-line-height:22px` with `--sand-font-size-lg:15px` |
|
||
| One-line bubble height | 36 px (7 px above and below) | `--sand-transcript-prose-one-line-height:36px`, `--sand-transcript-prose-block-padding:7px` |
|
||
| UI text sizes | 11/12/13/14/15/17/22/26 px, base 14 px | `--sand-font-size-xxs` … `--sand-font-size-xxxl` |
|
||
| Transcript item kinds | user, thinking, assistant-text, tool-call, send-message | `index.eager-app` function `zfn` |
|
||
| Thinking label | "思考中" | id `AUV+TY` |
|
||
| Message actions | 複製, 重新傳送, 停止 | ids `he3ygx`, `MyjAbr`, `NnvXp5` |
|
||
|
||
## Bots
|
||
| What | Value | Source |
|
||
|---|---|---|
|
||
| Bot fields | 名稱, 描述, 頭像 (shape + colour) | ids `RFtRvq`, `NDq8h1`, `fopc9x` |
|
||
| Avatar shapes | 26: arch, bean, blob, capsule, cloud, clover, crystal, cylinder, dome, egg, flower, gem, heart, hex, house, leaf, pebble, pumpkin, shield, sparkle, square, squircle, star, tablet, teardrop, wedge | `dist/electron-main/menubar-tray/assets/bot-*.png` |
|
||
| Rebuild shapes | 12 of them, drawn as simple shapes (look estimated) | own drawing |
|
||
|
||
## Look (light theme)
|
||
From `dist/renderer/assets/index-Dh7EWA8t.css`, block `:root,.sand-1vbcnu7`:
|
||
pane `#fff`, chrome `#fcfcfc`, sidebar `#f5f5f5`, text `#1c1c1c`, secondary `#1c1c1cad`,
|
||
tertiary `#1c1c1c8f`, hover `#1c1c1c0f`, active `#1c1c1c14`, border `#1c1c1c1f`,
|
||
user bubble `#1c1c1c` with white text, agent bubble `#1c1c1c0f`, focus `#1e86fc`, accent `#1680f3`.
|
||
Bot colours come from `--sand-fill-*-bubble-user`: red `#c82532`, orange `#d2550a`, yellow `#c2730b`,
|
||
green `#0d9655`, cyan `#0d9381`, blue `#126fd4`, purple `#7442d1`, magenta `#c82779`, brown `#86592e`.
|
||
|
||
Settings → 主題: 淺色 / 深色 / 跟隨系統 (ids `1njn7W`, `pvnfJD`, `Ea4oxV`).
|
||
強調色: Bot 顏色 / 黑色 (ids `p7v5PZ`, `THokF3`, description `eeSkqM`).
|
||
|
||
Dark theme colours weren't found in the stylesheet or the scripts: **estimated**.
|
||
Chat column 720 px: **estimated**. No transcript max-width was found; the 708, 640 and 960 px max-widths
|
||
in the CSS belong to other views.
|
||
|
||
## Computer tools and the work loop (taken over from the neighbouring deep investigation)
|
||
The deep investigation in `investigation.cany` (findings fnd_5–fnd_12) showed that the original's
|
||
agent loop runs on a remote "box". The desktop app only hosts a local tool runner
|
||
(`dist/local-exec-daemon/main.cjs`) and the permission layer. That session planned its own clean-room
|
||
harness (plan step 1 "doing", no code written yet). This version builds that plan here, in Rust, with
|
||
the model loop running locally against whichever provider the bot uses.
|
||
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| Search deadline | 25 000 ms | local-exec-daemon @2983388 `Mu.MAIN_TIMEOUT_MS=25e3` |
|
||
| Search hard cap | 10 000 lines | same, `Mu.HARD_MAX_OUTPUT_LINES=1e4` |
|
||
| Lines handed to the model | 2 000 | @2966784 `Jut=2e3` (client limit) |
|
||
| Largest file read | 8 388 608 bytes (8 MiB) | @2951190 `gut` |
|
||
| Long command | not killed: moved to the background, read later | @3010873 `Mye` (stop kind "background", reason TIMEOUT), @3016213 `timeoutBehavior===BACKGROUND` |
|
||
| Background read-back | `read_terminal` | @2959283 `readTerminalFile` |
|
||
| Command output kept | start + end, middle counted | ShellFailure fields `output_head`, `output_tail`, `elided_chars` (@2186946); 6 000 + 6 000 chars **estimated** |
|
||
| Foreground wait before background | 30 s, model may ask up to 600 s | **estimated** (the original gets it per call from the server) |
|
||
| Permission order | refuse > hidden files > allowed > ask | fnd_8: `shouldBlockShellCommand`, `isShellCommandFullyAllowlisted`, cursorignore/repo-block globs, approval callbacks |
|
||
| Chained commands | split on `&&`, `||`, `;`, `|` and checked one by one; `$(…)`, backticks, `eval` always ask | fnd_8 (tree-sitter-bash parsing), own splitter |
|
||
| "Always allow" | remembered per bot (e.g. "修改檔案", "npm install") | fnd_12 `recordLocalToolStandingGrant` |
|
||
| Never claim done without a result | in the bot's instructions | fnd_10 design lesson (paraphrased, not copied) |
|
||
| Summarize at | 70 % of the context | main-app.cjs `client_speculative_summarization_config.tokenUsageThresholdPercentage:70` |
|
||
| Extra room mid-task | +5 % (summarize again at 75 % between steps) | same config, `tolerancePercentage:5` (how it's applied is our reading) |
|
||
| Context size | 128K tokens by default, per bot | **estimated** |
|
||
| Token estimate | 4 Latin chars or 1 Chinese char ≈ 1 token | **estimated** |
|
||
| Steps per turn | 25 | **estimated** |
|
||
| Messages kept word for word | newest 6, cut only before a user message | **estimated** |
|
||
|
||
### Native helpers behind the tools (decompiled)
|
||
|
||
**`cursor_proclist.node`** (Mach-O arm64 bundle, 125 536 bytes, sha256 d5df3fb7…), the original's
|
||
process scanner. Strings found with native_strings_find (evidence `ev_3d585c56…`), and functions read with
|
||
native_function_pseudocode:
|
||
|
||
| Address | Function | Rule taken |
|
||
|---|---|---|
|
||
| `0x6748` | `getDescendantPids(vector<int>, bool)` | Breadth-first over children: `proc_listpids(6 = PROC_PPID_ONLY, pid, buf, 0x1000)`, de-duplicated in a hash set, and the loop stops once `0xfff` pids are collected, **at most 4096 processes** (evidence `ev_768ea7aa…`) |
|
||
| `0x3d00` | `ProclistScanWorker::Execute` | Calls `getDescendantPids(roots, true)` (roots included), then `getProcessInformation` for each and drops any that fail (evidence `ev_6aba01a8…`) |
|
||
| `0x5234` | `getProcessInformation(int, ProcessInfo&)` | `proc_pidinfo(pid, 3 = PIDTBSDINFO, 0x88)` for the parent; name via `proc_name`, else "<unknown>"; arguments via `sysctl` KERN_PROCARGS2 only when under `0x40000` bytes; agent tags read from the environment (`…CONVERSATION_ID=` 0x17 chars, `…REQUEST_ID=` 0x12 chars); `proc_pidinfo(pid, 4 = PIDTASKINFO, 0x60)`: **memoryMB = resident bytes >> 20**, **cpuTimeMs = (user + system) × timebase.numer / timebase.denom / 1 000 000** (magic-number divide `0x431bde82d7b634db >> 18`) (evidence `ev_257a505c…`) |
|
||
| `0x1fdc`, `0x6cc0` | `cursorProclistSystemMemory`, `getSystemMemoryInfo` | Reads `kern.memorystatus_vm_pressure_level` (not used here) |
|
||
|
||
How the rebuild applies it (app tools.rs `descendants`, `tree_stats`, `kill_tree`): every command is
|
||
tagged with `ANYBOT_CONVERSATION_ID` (the bot) and `ANYBOT_REQUEST_ID` (the tool call). These are our
|
||
own names for the original's vendor-prefixed tags. Pressing stop, or quitting, ends the command **and
|
||
everything it started** (up to 4096 processes, newest first), not just the shell. `read_terminal` reports
|
||
process count, CPU seconds and memory MB with the formulas above (via `ps`).
|
||
|
||
**`sand-op-launcher`** (universal Mach-O, 136 368 bytes, sha256 1337d620…). Strings in evidence
|
||
`ev_7d29f77e…`; `_main` read with native_function_pseudocode (evidence `ev_8006d51c…`):
|
||
- It verifies the 1Password CLI's code signature (`SecStaticCodeCheckValidity` against
|
||
`identifier "com.1password.op" … OU 2BUA8C4S2C`), and re-checks the file before `execve` ("changed before launch").
|
||
- It accepts only exact argument shapes (`item move`, `item list`, `vault list/create`, `account list`,
|
||
`service-account create`, `--version`); anything else is refused ("unsupported invocation").
|
||
- It applies the sandbox profile `(version 1)(allow default)(deny file-read* (with no-report) (subpath (param "ONEPASSWORD_GROUP_CONTAINER")))`.
|
||
- It builds a fresh environment (`OP_CACHE=false`, `OP_BIOMETRIC_UNLOCK_ENABLED=true`, `OP_LOAD_DESKTOP_APP_SETTINGS=false`).
|
||
|
||
How the rebuild applies it (app tools.rs `build_command`): on macOS every command runs under
|
||
`sandbox-exec` with the same profile pattern, extended to deny reading **and writing** the app's own data
|
||
folder (where API keys are saved), `~/.ssh`, `~/.gnupg`, `~/Library/Keychains`, `~/.aws` and
|
||
`~/.config/gcloud`. Paths are resolved first, because the sandbox matches real paths. The provider API-key
|
||
variables are removed from the command's environment, and the app's data folder is also on the
|
||
never-read list for file tools.
|
||
|
||
Read-only commands that run without asking: ls, pwd, cat, head, tail, wc, echo, rg, grep, find, file,
|
||
stat, du, df, date, which, tree, sort, uniq, diff, git status/diff/log/show (own list). Always refused:
|
||
sudo, su, rm -rf /, rm -rf ~, mkfs, dd, shutdown, reboot, diskutil erase. Never read or written:
|
||
.git internals, .ssh, .gnupg, Keychains, .env files, *.pem, *.key, id_rsa/id_ed25519, .netrc.
|
||
|
||
## Turns, permission hand-over, computers and app control
|
||
|
||
### What counts as one turn
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| A turn starts with | a person's message, or a "wake" (a routine or other trigger firing) | main-app.cjs @3100283 `w3n`/`P3n`: turn counter `n+=1` only for kind `human` or `wake` (`SimulatedMsgReason.SUBSCRIPTION` → wake) |
|
||
| A message sent while it works | steers the running turn and is **not** a new turn | same, `P3n`: `e.turnSteer===!0 ? null` (steer messages don't count) |
|
||
| Turn outcomes | success, cancelled, error | node-agent-coordinator `wl=["success","cancelled","error"]`, read back by `k6` as {turnId, outcome, settledAtMs} |
|
||
| Bot is idle only when | not running, no helper tasks running, not waiting for the person, no computer hand-off pending | node-agent-coordinator `ZF`: `!isRunning && hasRunningSubagents!==true && awaiting===undefined && boxHandoff===undefined` |
|
||
|
||
Rebuild (app agent.rs, app.rs): a message typed during a turn goes into the turn's steer queue and
|
||
reaches the model at its next step. If the model has just finished, the turn keeps going. Anything that
|
||
arrives after the turn has settled starts the next turn. Each turn records {turn id, outcome,
|
||
settled time}: stop is cancelled, a failure or stall is error, otherwise success.
|
||
|
||
### Computers
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| Default place to work | the bot's own computer ("Bot 的電腦", yGkA3X) | settings map `b$.computers` / `y$.botComputer` |
|
||
| This Mac | only after the person turns on "允許 Grok Bot 在這台電腦上執行任務,而不只是在它自己的電腦上" | setting `local-execution`, ids fuUvu2 / Hd1KB+ |
|
||
| Approvals name the machine | "允許 Grok Bot 在 {targetMachineDisplay} 上使用 {app} 嗎?", "請在 {targetMachineDisplay} 上回應" | ids eQViQE / ykDLyL |
|
||
| Taking over the bot's computer | "接管" (take over), then hand back | id yqwjpd; renderer `handBack`/`handBackForeverBox` @2601598 (not rebuilt; see below) |
|
||
|
||
Rebuild: "Bot 的電腦" is a sealed folder per bot (`~/AnyBot 工作區/bot-N`). Its commands run under
|
||
the macOS sandbox with nothing else in the home folder readable or writable, like a separate machine, so
|
||
it works there without asking. "你的電腦" is this Mac with the layered permission rules, and only after the
|
||
setting is on. Other computers are added in Settings → 電腦 and reached over SSH (read-only commands run
|
||
directly, others ask). Every tool has a `computer` choice, so "在我的 Mac 上…" picks this Mac.
|
||
|
||
### Permission hand-over
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| Rule choices | 先詢問 / 每次詢問 / 一律允許 / 永不允許 | ids 4YjKLA, 6CTZeX, NoKBgy, wvd4WD |
|
||
| Auto-review | "依照自訂規則審核 Bot 的操作", two lists: 「無需詢問直接允許」(continue automatically) and 「一律詢問」(stop and ask) | ids 9yhl+F, geCUKn/JOecNC, 1YRRmk/Pca/iV |
|
||
| Conflicts | "規則衝突時,「先詢問」優先"; built-in safety checks always apply | ids NFRGrQ, n9YiIv |
|
||
| Standing grants | "always allow" remembered (machine-scoped in the original) | fnd_12 `recordLocalToolStandingGrant` |
|
||
|
||
Rebuild: built-in refusals run first. For an action that would ask, auto-review (when on) asks the
|
||
model to match it against the two rule lists, and an "always ask" match wins. "Allow" continues without
|
||
a card, and the result notes which rule matched. Otherwise the approval card names the computer.
|
||
|
||
### Asking for permission when it's needed (pop-up)
|
||
The original asks on the spot and names the machine ("允許 Grok Bot 在 {targetMachineDisplay} 上使用
|
||
{app} 嗎?", eQViQE; "請在 {targetMachineDisplay} 上回應", ykDLyL). The rebuild never lets a step fail
|
||
quietly for lack of permission:
|
||
- Computer tools are always offered to the model. If they're off for this bot, the step asks
|
||
"允許 {bot} 使用電腦工具嗎?": 允許這一次 / 一律允許這個 Bot 使用電腦 (turns the bot's tools on) / 拒絕.
|
||
- If the step targets 你的電腦 while 在這台電腦上執行 is off, it asks "允許 {bot} 在你的電腦上執行任務嗎?"
|
||
(wording from Hd1KB+): 允許這一次 / 一律允許 (turns the setting on) / 拒絕.
|
||
- Every request, including single actions, shows as a centred pop-up over a dimmed window, even when
|
||
another bot's chat is open. The Dock icon bounces (`RequestUserAttention`) when AnyBot isn't in front.
|
||
"稍後再決定" hides the pop-up; the chat card, the header pill "● 需要你允許" (click to reopen) and the
|
||
sidebar's highlighted "需要你允許" stay until answered.
|
||
- The bot is told to just call the tool and let the app ask, rather than telling the person to change
|
||
settings. Tested by `missing_permission_pops_up_and_continues`.
|
||
|
||
### Notifications when you're away (decompiled)
|
||
**`sand_notification_settings.node`** (Mach-O arm64, 102 368 bytes, sha256 6b2b9fa3…). Strings in evidence
|
||
`ev_a3c3cb3d…`, functions read with native_function_pseudocode:
|
||
|
||
| Address | Function | Rule taken |
|
||
|---|---|---|
|
||
| `0x35b4` | `getNotificationSettings(NotificationSettingsInfo&)` | Only on macOS ≥ 10.14 (`__isPlatformVersionAtLeast(1,10,14,0)`) and only inside a real `.app` bundle (bundlePath must end in ".app"). Asks `UNUserNotificationCenter` and waits on a semaphore for at most **3 s** (`dispatch_time(0, 0xb2d05e00)` ns). `authorizationStatus` 0..4 → notDetermined/denied/authorized/provisional/ephemeral, otherwise "unknown"; `alertSetting` 0..2 → notSupported/disabled/enabled, otherwise "unknown" (evidence `ev_7feb06d3…`) |
|
||
| `0x3898` | its completion block | Stores authorizationStatus and alertSetting, then signals the semaphore (evidence `ev_32393aac…`) |
|
||
| `0x2de8` | `NotificationSettingsWorker::OnOK` | Resolves `{platform:"darwin", setting, alertSetting}` (evidence `ev_ec969729…`) |
|
||
|
||
From main-app.cjs:
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| Permission mapping | authorized/provisional/ephemeral → granted, denied → denied, notDetermined → undetermined | @3183391 `hBt`, `mur` |
|
||
| Banners allowed | only when authorized **and** alerts enabled | `fBt` |
|
||
| You count as present | signed in, screen not locked, not idle, window alive | `K4` |
|
||
| Idle after | **90 s** without input | `Z_e=9e4` (10 min `cBt=10*6e4` while a phone push is being held) |
|
||
| Presence re-checked every | **10 s** | `uBt=1e4` |
|
||
| Setting text | "Bot 完成任務或等你處理時通知你" | id ttdOeI |
|
||
|
||
Rebuild (notify.rs): presence = window in front **and** input within 90 s (idle read from macOS
|
||
`HIDIdleTime` every 10 s). When you're not present and a bot needs permission, or finishes a turn, a
|
||
macOS banner appears with a sound, on top of the pop-up and the Dock bounce (Settings → 一般 → 通知,
|
||
on by default). A build-folder app isn't a `.app` bundle, so, as the original's module shows, it can't
|
||
read its own notification permission. Banners go through macOS's `display notification` instead, which
|
||
may need allowing once under System Settings → Notifications. The 3 s settings read and the permission
|
||
mapping aren't used for that reason.
|
||
|
||
### 直接操作 (work like Grok Bot without a stream of questions)
|
||
Why the original rarely asks: it does most work on **its own cloud computer and browser** ("Bot 的電腦"),
|
||
so nothing on the Mac needs permission. Working on the Mac is one switch (`local-execution`,
|
||
Hd1KB+), after which **auto-review** checks actions instead of the person ("讓 Grok Bot 在你的電腦上開啟
|
||
檔案和執行任務。自動審核仍會先檢查所有操作", ENHi8c; hrkSSQ). Per-app grants are remembered
|
||
(`recordLocalToolStandingGrant`, `listComputerUseAllowedApps`, fnd_12).
|
||
|
||
Rebuild: **Settings → 電腦 → ⚡ 直接操作**, or the "⚡ 全部允許,之後直接操作" link on any request.
|
||
- One switch turns on 在這台電腦上執行, every bot's computer tools and auto-review. Running turns stop
|
||
asking at once, and other bots' waiting "may I use the computer" requests are answered yes.
|
||
- Actions that would ask go ahead with the note "直接操作模式:已自動允許". The only exceptions are
|
||
actions that match an "always ask" rule (checked by the model, as in auto-review). The default rules
|
||
are own wording: 刪除檔案或資料夾 / 付款、購買或轉帳 / 用我的帳號發文、留言,或傳訊息給別人.
|
||
- Still blocked: the built-in refusals (sudo, mkfs, rm -rf /…), secret files and AnyBot's own data
|
||
(sandbox), as in the original ("內建的安全檢查一律有效", n9YiIv).
|
||
- macOS's own one-time prompt to let AnyBot control a browser can't be skipped by any app.
|
||
- Tested by `direct_mode_asks_nothing_but_still_blocks_danger`.
|
||
|
||
**Admin locks (decompiled).** `sand_notification_settings.node` `managedPolicyValueSync` at `0x1870`
|
||
(evidence `ev_86419910…`) takes `(productName, valueName)`, both of which must be strings (napi type 4),
|
||
otherwise it throws "managed_policy_value_sync(productName, valueName)". On macOS it then returns null:
|
||
it reads nothing on a Mac. The app side (main-core.cjs @727949, `EZ`/`TZ`/`Ak`/`mZ`/`gZ`) reads policy
|
||
from **macOS user defaults** (`systemPreferences.getUserDefault(key, "string")`, which MDM profiles can
|
||
force), from `~/.cursor/policy.json` on Linux, and from the native call on Windows. Missing or blank
|
||
values mean **unmanaged**, a valid value means **pinned**, and anything else is **invalid** (its URLs must be
|
||
plain https origins).
|
||
|
||
Rebuild (policy.rs): the same three kinds for two switches, read from macOS defaults domain
|
||
`com.anybot.AnyBot` (`AllowDirectMode`, `AllowLocalExecution`; Linux: `~/.anybot/policy.json`):
|
||
- unmanaged lets the person decide; pinned true/false fixes it; invalid locks it **off** (fail safe).
|
||
- A locked switch is greyed out with "🔒 由你的管理員設定,無法變更". The ⚡ link is hidden.
|
||
- A locked "你的電腦" is refused at once instead of asking. Locks are re-read each time Settings opens.
|
||
- Example: `defaults write com.anybot.AnyBot AllowDirectMode false`.
|
||
|
||
### Websites and the person's browser
|
||
The original reaches sites through its own browser (its old Chrome login copy is retired, see below).
|
||
The rebuild (web.rs) gives every bot:
|
||
- `web_fetch`: public pages as text (20 s timeout, 3 MB read, 20 000 characters to the model: **estimated**).
|
||
No permission needed.
|
||
- `browser_open`: opens a link in the person's own browser on this Mac (Chrome, Arc, Edge, Brave,
|
||
Chromium or Safari, whichever is open), where they're already logged in, and waits 4 s (**estimated**).
|
||
- `browser_read`: title, address and visible text of the current tab, via AppleScript. It asks first
|
||
(grant 「讀取瀏覽器分頁」).
|
||
- Both browser tools count as working on 你的電腦, so the computer pop-ups apply when those are off.
|
||
macOS asks itself the first time AnyBot controls a browser. If that's refused (-1743), the app opens
|
||
System Settings → 隱私權與安全性 → 自動化 and says what to switch on. If page text is blocked, it says
|
||
which menu item to turn on (Chrome: 檢視 → 開發人員 → 允許來自 Apple 事件的 JavaScript).
|
||
- **Which browser** (decompiled). `sand_window_capture.node` (Mach-O arm64, 128 064 bytes, sha256
|
||
f2b48533…; strings in evidence `ev_6c197ec3…`):
|
||
|
||
| Address | Function | Rule taken |
|
||
|---|---|---|
|
||
| `0x52c0` | `listWindows()` | `CGWindowListCopyWindowInfo(0x11, 0)`: on-screen windows only (`0x1`) without desktop elements (`0x10`), front to back. A window is kept only if its alpha > 0 and its bounds have width > 0 and height > 0. Each entry records window id, owner pid, layer, bounds and owner name (evidence `ev_fbc46bcb…`) |
|
||
| `0x5258` | `screenCaptureAccess()` | macOS ≥ 14 (`__isPlatformVersionAtLeast(1,14,0,0)`), else 2 = unsupported; otherwise `CGPreflightScreenCaptureAccess() ^ 1` (0 = granted, 1 = not granted) (evidence `ev_e0e9c7c2…`) |
|
||
| `0x651c` | `statusForError(NSError*)` | error code −3801 (the user declined capture) → status 2 "denied"; any other → 5 "failed" (evidence `ev_7c417898…`) |
|
||
|
||
Rebuild: `browser_read`/`browser_open` read the same front-to-back window list (through macOS's own
|
||
JavaScript bridge) with the same filter, plus layer 0 for ordinary app windows (ours), and use the
|
||
browser whose window is **closest to the front**, the tab the person is actually looking at. If no
|
||
browser window is visible they fall back to the first open browser, then Chrome, then Safari.
|
||
Screen capture itself isn't rebuilt: reading the page text needs no screenshot.
|
||
- The bot is told not to say it has no tools, and to try these first. Checked against xAI grok-4.7:
|
||
"幫我看一下我的threads 看一下我的帳號" now gives `browser_open(threads.net)` + `browser_read`.
|
||
|
||
### AnyBot's own browser (replaces driving the person's browser)
|
||
Grok Bot never asks the person to change browser settings because it browses in **its own browser**
|
||
(its Chromium-based browser and webviews on the bot's computer). Driving the person's Chrome through
|
||
AppleScript, the first approach here, needed Chrome's "允許來自 Apple 事件的 JavaScript" plus a macOS
|
||
Automation prompt.
|
||
|
||
**Copying logins from Chrome: retired in the original, not rebuilt.**
|
||
- Native main program (`Grok Bot` Mach-O, opened with core_target_open): searching its strings for
|
||
browser / chrome / cdp / devtools / remote / port / debug finds **0** strings (evidence
|
||
`ev_3dc3f004…`), so no browser or cookie logic lives in native code; it's all in the JavaScript.
|
||
- The renderer still has the old approval card (chunk-view @3041: 「{botName} 想要使用你現有的登入」,
|
||
「從你電腦上的 Chrome 複製 Cookie…」, 檢視網站 / 允許一次 / 一律允許 / 拒絕, sites grouped by Chrome
|
||
profile), but the error catalogue in main-core.cjs and index.eager-app (domain `chrome-import`,
|
||
codes **SAND-E0801 … SAND-E0812**: cookieApprovalNoMachine … chromeCookieImportFailed) marks every
|
||
one "retired: the agent-prompted cookie sync that raised it was removed" / "retired: the Chrome
|
||
cookie import that raised it was removed", and the inject was behind a gate
|
||
(`sand_import_chrome_cookies`, SAND-E0811).
|
||
- So the shipping original no longer copies Chrome cookies. The rebuild matches that: nothing is
|
||
copied, and the bot is told never to offer it. (Also: this Mac's Chrome/Brave data folders refuse
|
||
reads without Full Disk Access, "Operation not permitted", so it couldn't work quietly anyway.)
|
||
The person logs in once in AnyBot's own browser window instead.
|
||
|
||
Rebuild (botbrowser.rs): AnyBot starts a separate Chrome-family profile (Chrome, Brave, Edge or
|
||
Chromium, whichever is installed) as **its own browser window**, with logins kept in
|
||
`…/AnyBot/browser`. It talks to it over Chrome's DevTools protocol on local port **9333**
|
||
(**estimated**, overridable for tests):
|
||
- browser_open: starts the window (waits up to 20 s, **estimated**) or adds a tab (`PUT /json/new?url`).
|
||
- browser_read: picks the first real page tab (`/json/list`) and reads
|
||
`{title, location.href, document.body.innerText}` with `Runtime.evaluate` (20 s limit, **estimated**).
|
||
- No browser setting, no Automation prompt, no "your computer" question: it's the bot's browser.
|
||
The person logs in to a site in that window once; a short page mentioning 登入 / log in gets a note
|
||
asking them to.
|
||
- The bot is told never to send the person to browser menus. The AppleScript path remains only as a
|
||
fallback when no Chrome-family browser is installed (Safari only).
|
||
- Checked live against a hidden Chrome: example.com was read as "標題:Example Domain".
|
||
|
||
### Browser profiles (設定檔)
|
||
Grok Bot lets the person use any Chrome profile because on "your computer" it drives the real screen
|
||
through its Computer Use helper app (local-exec-daemon: `Grok Bot Computer Use`,
|
||
`co.anysphere.grok-bot-computer-use`, `CUGrokBotService`): it clicks in their normal Chrome like a
|
||
person would. The original has no browser-profile code of its own (no `--profile-directory`,
|
||
`Local State` or `info_cache` anywhere; `profileDisplayName` only on the retired login-copy card).
|
||
AnyBot doesn't control the screen, and its own browser couldn't see the person's Chrome profiles
|
||
(macOS refuses other apps `…/Google/Chrome/Local State`: "Operation not permitted"). So
|
||
(rebuild, **estimated** rules):
|
||
- AnyBot's browser has its own profiles: 「預設」 (the existing `…/AnyBot/browser` folder, logins kept)
|
||
plus any added ones in `…/AnyBot/browser-profiles/<name>`. Each is its own window with its own logins.
|
||
- Names: 1–30 characters, no `/` or `:`, not starting with `.`; matched ignoring case.
|
||
- 「預設」 keeps port 9333; added profiles start with port **0**, so Chrome picks a free port and writes
|
||
it to `DevToolsActivePort` in the profile folder, which AnyBot reads back (checked live: Chrome 156
|
||
wrote 62337 and answered on it).
|
||
- The profile in use is remembered in `…/AnyBot/browser-profile.txt`.
|
||
- Bot tools: `browser_profiles` (list / add / use, no permission needed: it's the bot's browser), and
|
||
an optional `profile` on browser_open / browser_read (opening in a profile makes it the one in use).
|
||
The bot is told to use these when the person says profile / 設定檔 / another account.
|
||
- Settings → 電腦 → 「AnyBot 瀏覽器設定檔」: pick the one in use, 「開啟視窗」 to log in, 「新增設定檔」.
|
||
Removing a profile isn't offered (it would delete its logins).
|
||
|
||
### 「你的瀏覽器」: reading the person's own Chrome, any profile (decompiled)
|
||
How Grok Bot reads the person's real Chrome in whatever profile is open: its Computer Use helper
|
||
(`Helpers/Grok Bot Computer Use.app/Contents/MacOS/CUGrokBotService`, Mach-O fat, sha256
|
||
1004a8cb…) turns on Chrome's accessibility tree and reads the screen's elements. Strings:
|
||
`com.google.Chrome`, `com.google.Chrome.`, `Google Chrome Framework.framework` (evidence
|
||
`ev_75dff652…`), `AXManualAccessibility` / `AXEnhancedUserInterface` (`ev_bef6c29e…`), the 3
|
||
framework names (`ev_a8cda95a…`). Functions read:
|
||
|
||
| Address | Function | Rule taken |
|
||
|---|---|---|
|
||
| `0x1000a1b6c` | `CURendererApp.isRenderer(bundleIdentifier:executablePath:)` | renderer if bundle id == `com.github.Electron` or == `com.google.Chrome`, or has prefix `com.google.Chrome.`, or the executable path contains `/Electron.app/Contents/MacOS/Electron`, else `isRendererBundle` (evidence `ev_4973f594…`) |
|
||
| `0x1000a19ec` | `CURendererApp.isRendererBundle(atPath:)` | `<bundle>/Contents/Frameworks/<name>` exists for one of 3 names: `Electron Framework.framework`, `Chromium Embedded Framework.framework`, `Google Chrome Framework.framework` (evidence `ev_f0dc9836…`) |
|
||
| `0x1000a1cdc` | `CURendererApp.isRenderer(NSRunningApplication)` | takes the running app's bundle id, bundle URL path and executable URL path, then calls the rule above (evidence `ev_179b4f10…`) |
|
||
| `0x1000a1554` | `CURendererAccessibility.writeFlags(on:)` | `AXUIElementSetAttributeValue(app, "AXManualAccessibility", on)` then `(app, "AXEnhancedUserInterface", on)`; result from both error codes (evidence `ev_f661d29e…`) |
|
||
|
||
Rebuild (axread.rs), used by the profile choice 「你的瀏覽器」 (aliases 我的 Chrome, 你的 Chrome…):
|
||
- Same renderer rule (`is_renderer`, constants copied as written) and the same two flags, in order, on
|
||
the person's browser before reading. Safari etc. need no flags.
|
||
- macOS asks once for 輔助使用 (`AXIsProcessTrustedWithOptions` with the prompt). If it's off, AnyBot
|
||
opens 系統設定 → 隱私權與安全性 → 輔助使用 and says to switch on AnyBot. No browser setting is needed.
|
||
- Reads the focused window → first `AXWebArea` → text of `AXStaticText` (`AXValue`), one line per
|
||
group; title `AXTitle`, address `AXURL`. Up to 20 000 elements, depth 80, 5 tries 0.7 s apart
|
||
while Chrome builds the tree, 2 s per answer (all **estimated**).
|
||
- The person's browser process is told apart from AnyBot's own window by its arguments (AnyBot's
|
||
runs with `--user-data-dir=…/AnyBot/browser`; helper processes carry `--type=`).
|
||
- browser_open on 「你的瀏覽器」 starts the browser's own executable with the link, so Chrome hands it
|
||
to the person's running window (their normal profile) instead of AnyBot's window.
|
||
- It counts as using 你的電腦: the computer pop-ups apply and reading asks first (「讀取你瀏覽器目前
|
||
分頁」), like before. Switching Chrome profiles is done by the person in Chrome; the bot reads the
|
||
window in front.
|
||
- Not checked live yet: this test process isn't allowed 輔助使用 (trusted=false).
|
||
|
||
### Talking to the bot runs the app
|
||
Routines are made in chat ("例行任務是這個 Bot 依排程重複執行的任務。請在聊天中請它設定一個。", ZJmAzX),
|
||
and the primary bot can coordinate other bots (mQQ/Uq). App tools in the rebuild (appctl.rs):
|
||
list_routines, create_routine, update_routine, delete_routine (asks first, wording VAgxC4),
|
||
run_routine_now, list_bots, create_bot, update_bot (provider, model, name, pin, default computer),
|
||
message_bot (wakes another bot), list_computers, update_settings (theme, accent, time zone). Turning
|
||
on "在這台電腦上執行" stays person-only.
|
||
|
||
### Routine schedules
|
||
Shape from index.eager-app @1508626 (`Prt`, `Irt`): `{frequency: daily|weekly|monthly|yearly, interval,
|
||
daysOfWeek: MO..SU (yA), monthDay | weekdayOfMonth {week, day}, month, time "HH:MM", timezone}`.
|
||
Descriptions use the original's zh-TW strings: 每天 / 每{n} 天 / 每個工作日 / 每週的{days} / 每月{d}日 /
|
||
每月的{week}{day} / 每年{date}, then "{repeats} {time}" and "({zone})" when not the person's time zone.
|
||
"Every weekday" means exactly MO–FR (`isEveryWeekday`, `WORKWEEK`). Next-run rules (ours): intervals
|
||
count from the creation date; a month day past the month's end runs on the last day; summer-time gaps
|
||
take the earliest valid time. A routine that comes due while the bot is busy waits until it is idle.
|
||
|
||
### Screen control on 你的電腦 (computer use)
|
||
The original's local daemon (local-exec-daemon main.cjs) drives its Computer Use helper with these
|
||
tools; the rebuild (computer.rs) uses the same names and arguments:
|
||
`computer_use_screenshot` {}, `computer_use_click` {method: coordinate, x, y, button, count,
|
||
modifier_keys}, `computer_use_scroll` {x, y, direction, amount, modifier_keys},
|
||
`computer_use_mouse_move` {x, y}, `computer_use_drag` {button, path, modifier_keys},
|
||
`computer_use_typing` {value}, `computer_use_press_key` {key, hold_duration_ms} (main.cjs @2860904).
|
||
- Screenshot size, `zct` (main.cjs): long edge fitted to **1024** (`Gct`); if the short edge is under
|
||
**8** (`Hs`) nothing is resized; after scaling, an edge under 8 is raised so the short edge is 8.
|
||
Largest image **1 048 576** bytes (`mg = 1024*1024`). JPEG is tried at quality **85, 70, 55** (`$3r`)
|
||
and the first that fits is used, else the smallest one if it's under the cap (`j3r`).
|
||
- Rebuild: `screencapture -x -m` of the main screen, fitted and encoded by those rules, sent to the
|
||
model as a picture (Claude: inside the tool result; OpenAI-style: one user message with the
|
||
pictures right after the run of tool results). Only the newest **3** screenshots are sent; older
|
||
ones say 「較早的截圖已省略」 (**estimated**). Screenshots aren't saved with the chat.
|
||
- Coordinates are pixels of the latest screenshot; AnyBot turns them into screen points with the
|
||
screen size (`CGDisplayBounds`) ÷ picture size. Shortcuts like `cmd+shift+t` use the US key codes.
|
||
- Mouse and keyboard timing, decompiled from `CUGrokBotService` (evidence ids after each):
|
||
|
||
| Address | Function | Rule taken |
|
||
|---|---|---|
|
||
| `0x100010120` | `CUActuator.click(pid:screenPoint:windowBounds:…button:count:holdSeconds:)` | event source `CGEventSourceCreate(1)` (HID system state) with `CGEventSourceSetLocalEventsSuppressionInterval(0)`; press → hold → release where hold = `holdSeconds > 0 ? holdSeconds : 0.03`; `count` clicks with **0.08 s** between them; error "Could not create click event"; background windows get a window-local click (fields 91/92 = window id, "skylight-no-raise") (`ev_f3077201…`) |
|
||
| `0x100010a24` | `CUActuator.typeCharacters(pid:text:)` | empty text → "`value` is required and must be a non-empty string"; same event source, then `postChunks` (`ev_77b2182b…`) |
|
||
| `0x1000beb00` | `CUTypedCharacters.utf16Chunks` | chunks of at most **20** UTF-16 units (buffer 0x14; a new chunk when the next character would reach 21), never splitting a character (`ev_0dce1c89…`) |
|
||
| `0x1000beebc` | `CUTypedCharacters.postChunks` | each chunk: `CGEventCreateKeyboardEvent(src, 0x80 = 128, down/up)` + `CGEventKeyboardSetUnicodeString`, flags 0; down → **0.035 s** → up; **0.02 s** between chunks; error "Could not create character keyboard event" (`ev_714c5253…`) |
|
||
| `0x100031764` | `CUCompanionToolService.handlePressKey` | presses a list of keys in order: each down → **0.035 s** → up, **0.02 s** between keys; result "Pressed {keys joined by spaces}."; error "Could not create keyboard event" (`ev_730d67a4…`) |
|
||
|
||
| `0x10008c34c` | `CURemoteEventPoster.drag(path:button:steps:holdSeconds:flags:)` | fewer than 2 points → "Drag path requires at least two points."; mouse down at the first point (click state 1), then if hold > 0 sleep it in 10 000 µs slices, then for every segment post `steps` dragged events at `start + delta × i / steps` (i = 1…steps, steps at least 1), then mouse up at the last point (`ev_f0d60f2e…`) |
|
||
| `0x10009dee4` | `CURemoteToolService.handleDrag` (closure) | calls drag with **steps 6** and **holdSeconds 0.04**; result "Dragged from (%.0f, %.0f) to (%.0f, %.0f) with left/right/middle button." (`ev_65c07c1e…`) |
|
||
| strings | `handleScroll` / `handleDrag` | "`direction` must be `up`, `down`, `left`, or `right`.", "`amount` must be an integer.", "`path` must be an array of at least two {x,y} points in model coordinates.", "Could not create scroll event" (`ev_0b3df00c…`) |
|
||
|
||
Drag in the rebuild: same press, 0.04 s hold, 6 interpolated steps per segment, release at the
|
||
end, and the reply 「已從 (x, y) 拖曳到 (x, y)(左鍵)」. Scroll refuses a non-integer amount.
|
||
|
||
| Address | Function | Rule taken |
|
||
|---|---|---|
|
||
| `0x10009e200` | `CURemoteToolService.handleScroll` | `direction` is lowercased and must be up/down/left/right; `amount` must be an integer, default **3**, kept to **1…20** (`max(amount,1)`, `min(…,20)`); distance = `(amount + amount) << 3` = **amount × 16** pixels; down and right are negative; `x` and `y` "must be provided together", and without them the current pointer position is used ("Could not read the current cursor position."); needs Accessibility and Screen Recording (`ev_98625ed7…`) |
|
||
| `0x10009e998` | handleScroll closure | posts a mouse-moved event (type 5) at the spot, then `CGEventCreateScrollWheelEvent2(src, units 0 = pixel, wheels 2, vertical, horizontal, 0)` at the same spot with the modifier flags; result "Scrolled {direction} x{amount}." (`ev_ee9d8372…`) |
|
||
|
||
Rebuild scroll: the same default, range, 16-pixel steps in pixel units, the both-or-neither x/y
|
||
rule (no screenshot needed when scrolling where the pointer is), and 「已捲動 down ×3。」.
|
||
Rebuild: the same event source, the same holds and gaps, the same chunking on key 128, and a key
|
||
press may be several keys separated by spaces (`cmd+a delete`). `hold_duration_ms` replaces the
|
||
0.035 s hold (at most 5 s, **estimated**). Window-local background clicks (clicking without
|
||
bringing the window forward) aren't rebuilt: AnyBot clicks where the pointer is, like a person.
|
||
Pauses between mouse-move and drag steps are 8 ms (**estimated**).
|
||
- Permissions: macOS asks once for 螢幕與系統錄音 (screenshots, `CGPreflightScreenCaptureAccess` like
|
||
`screenCaptureAccess()` @0x5258) and once for 輔助使用 (mouse and keyboard); if off, AnyBot opens the
|
||
right System Settings pane and says what to switch on. In AnyBot, looking at the screen asks once
|
||
(grant 「看你的螢幕畫面」) and using the mouse/keyboard asks once (「操作你的滑鼠和鍵盤」), both with
|
||
"always allow"; 直接操作 skips the questions except for "一律詢問" rules. It always counts as 你的電腦,
|
||
so the computer pop-ups and admin locks apply.
|
||
- Needs a model that can see pictures.
|
||
- Not checked live: this test process has neither permission.
|
||
|
||
### Still different from the original
|
||
- "接管" (take over the bot's computer) is not rebuilt: the bot's computer here is a sealed folder,
|
||
not a cloud machine with a screen. Screen control works on 你的電腦 only.
|
||
- Routine triggers from events (GitHub, PagerDuty, Sentry: `GITHUB_EVENT_KINDS` etc.) are not rebuilt;
|
||
only time schedules.
|
||
- The look still uses the estimated layout: the signed-in original's window hasn't been captured.
|
||
|
||
## Restarting and recovering
|
||
|
||
### From the native main program (decompiled again for this feature)
|
||
`Contents/MacOS/Grok Bot`. native_strings_find for `relaunch|restart|ELECTRON|dev/null|crash` finds two
|
||
strings (evidence `ev_6af348eb…`), and both lead to these functions:
|
||
|
||
| Address | Function | Rule taken |
|
||
|---|---|---|
|
||
| `0x1000004c0` | `sub_1000004c0` | Before anything else, for fd 0/1/2: if `fstat` fails with `errno == 9` (EBADF), `freopen("/dev/null", …)`, so a copy started without terminal streams still has valid ones (evidence `ev_29ffc0d8…`) |
|
||
| `0x100000460` | `entry` | Calls `0x1000004c0`, then a set `ELECTRON_RUN_AS_NODE` (fuse on, non-empty) changes how the program starts (`ElectronInitializeICUandStartNode` instead of `ElectronMain`) (evidence `ev_7c151be5…`) |
|
||
|
||
### From the app code
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| Relaunch helper | detached shell, all output ignored: `spawn(shell, ["-c", script, "grok-bot-relaunch", pid, app, …args], {detached:true, stdio:"ignore"})` | main-app.cjs @3195964 `Pur` |
|
||
| Wait before starting | `while kill -0 "$pid" 2>/dev/null; do sleep 0.2; done; exec "$app" "$@"`: the new copy starts only after the old one exited, polled every **0.2 s** | same, `PBt` |
|
||
| Inherited files | closes fds > 2 first (`rEe`, Linux /proc only) | same |
|
||
| Button while it happens | "正在重新啟動…" (Restarting…) | renderer `ljn` → id fvg2KT |
|
||
| Bot's computer stuck | "Grok Bot 的電腦卡住時,從你上次儲存的快照重新啟動它"; recovering keeps files and logins, removes installed apps and packages | ids O+wR5I, ixKoax |
|
||
|
||
### How the rebuild applies them
|
||
- **重新啟動 AnyBot** (Settings → 一般): stops running turns as cancelled and background commands, saves,
|
||
then starts a detached `/bin/sh` helper. The helper waits for this copy's pid to disappear, checking
|
||
every 0.2 s, and then starts the app, so the two copies never overlap and the last save can't be
|
||
overwritten. Its input and output go to `/dev/null`. Start-up switches (`ANYBOT_SHOT_VIEW`, the
|
||
per-command tags, `ELECTRON_RUN_AS_NODE`) are removed so the new copy opens normally. The button reads
|
||
"正在重新啟動…" while it happens. Tested by `relaunch_waits_for_the_old_copy`.
|
||
- **重新啟動這個 Bot** (⋯ menu): ends the bot's turn as cancelled, stops its background commands and
|
||
everything they started (by its conversation tag), and marks half-finished replies as interrupted.
|
||
Chats and the files on its computer are kept. The snapshot "recover" step isn't rebuilt: the bot's
|
||
computer here is a folder, not a machine image.
|
||
- **Interrupted turns on load**: a reply that was still streaming when the app closed is saved empty and
|
||
can't continue. On load it becomes "(這次的回應被中斷了…)" with an error settlement, so "重新產生"
|
||
appears. A message with no reply shows "重新傳送" (MyjAbr).
|
||
|
||
## Versions and updating
|
||
|
||
**Native main program:** native_strings_find for `version|update|0.78|x.y.z` in `Contents/MacOS/Grok Bot`
|
||
finds **0 strings** (evidence `ev_df0e0f59…`). The version (0.78.1) lives only in Info.plist
|
||
(`CFBundleShortVersionString`), and all update logic is in main-app.cjs.
|
||
|
||
| Rule | Value | Source |
|
||
|---|---|---|
|
||
| First check after launch | 30 s | main-app.cjs @3372407 `bhr=3e4` (`initialCheckDelayMs`) |
|
||
| Then every | 1 h + random 0–5 min | `khr=3600*1e3`, `Ihr=300*1e3`, `nextPeriodicDelayMs = interval + floor(random·jitter)` |
|
||
| Apply by itself only when | auto-update on **and** update ready **and** (screen locked **or** screensaver running) **and** idle ≥ **600 s** **and** no agent work | @3365216 `uQt`, `phr`, `pQt=600`, `uhr` (`confirmed-idle`) |
|
||
| Auto-update default | on | `getAutoUpdateWhenIdle?.() ?? true` |
|
||
| Screen locked | `powerMonitor.getSystemIdleState(1) === "locked"` | `Aj`/`Cle` |
|
||
| Screensaver running | `pgrep -x ScreenSaverEngine` (2 s timeout) | `NRn`, `DRn` |
|
||
| Labels | "重新啟動以更新" (FzpXvM), "正在重新啟動…" (fvg2KT), "在你離開時自動更新用戶端" (4vprnG) | catalogs |
|
||
|
||
Rebuild: the version shown is the program file's build time (sidebar footer, Settings → 一般 → 版本).
|
||
A newer build on disk shows the sidebar button "⬆ 重新啟動以更新(time)". Noticing it is a file check
|
||
every 10 s (**estimated**; it's local and cheap). Switching by itself follows the original's schedule:
|
||
first try 30 s after start, then hourly plus jitter, and only when the screen is locked or the
|
||
screensaver is running (`ioreg` session flag / `pgrep -x ScreenSaverEngine`), idle ≥ 600 s, and no
|
||
bot is working or waiting for you. It uses the same wait-then-start relaunch helper. Setting: 在你離開時
|
||
自動更新用戶端 (on by default). Tested by `update_applies_only_when_truly_away`.
|
||
|
||
## Left out of this version
|
||
Done since this list was first written: routines (例行任務), screen control on 你的電腦, AnyBot's own
|
||
browser with profiles, reading the person's own Chrome, removing an added browser profile.
|
||
Still left out: Slack/Telegram/WhatsApp/email messaging, team bots and sharing, payments (these need the
|
||
original's cloud service and accounts), routines started by events (GitHub, PagerDuty, Sentry need
|
||
webhooks into that cloud), "接管" of a cloud bot computer, voice input, file attachments, and the exact
|
||
signed-in window layout and dark-theme colours (never captured).
|
||
|
||
## Look check (full pass over every screen)
|
||
- One font for everything: PingFang TC Regular (macOS system asset, found under
|
||
`/System/Library/AssetsV2/com_apple_MobileAsset_Font*/…/PingFang.ttc`, face 2), falling back to
|
||
Hiragino Sans GB. Before, English came from egui's own font and Chinese from a fallback, on
|
||
different baselines and sizes. egui's fonts stay behind it only for missing symbols.
|
||
- That font's 「·」 already has space on both sides, so separators are written without extra spaces.
|
||
- Sidebar names and previews end in "…" before the status dot instead of being cut mid-letter.
|
||
- A step's copy/model line only shows under the bot's final words, so a tool's result sits right
|
||
under the step that asked for it.
|
||
- Settings and Bot 設定 stop 80 px short of the window height (**estimated**) and scroll inside;
|
||
the provider list is 200 px wide (**estimated**) so 「自訂(OpenAI 相容)」 fits on one line;
|
||
all avatar shapes fit on one row; the model list button says 「選擇」.
|
||
- Decompile note: the native main program (`Grok Bot`, Mach-O 53 040 bytes, sha256 df5b07de…) is
|
||
only Electron's launcher: a search for font / width / height / title finds **0** strings
|
||
(evidence `ev_7cf8e974…`). The look lives in the renderer styles (index-Dh7EWA8t.css), read here:
|
||
- Type scale `--sand-font-size-*`: xxs 11, xs 12, sm 13, base 14, lg 15, xl 17, xxl 22, xxxl 26 px;
|
||
`--sand-line-height-*`: 16, 16, 18, 20, 22, 24, 28, 32 px.
|
||
- `--cursor-font-family-sans`: `-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`, which on
|
||
a zh-TW Mac means SF for Latin and PingFang for Chinese. `--cursor-font-family-mono`:
|
||
`ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, …`.
|
||
- Rebuild: body/buttons 14 (base), small 12 (xs), headings and window titles 17 (xl), code 13 (sm)
|
||
on 18 px lines, chat text 15 (lg) on 22 px lines, Markdown headings 17 on 24 px lines; code in SF
|
||
Mono (`/System/Library/Fonts/SFNSMono.ttf`, then Menlo). Text stays in PingFang TC for both
|
||
scripts, which matches -apple-system's Chinese and is close to SF for Latin (egui can't line up
|
||
two fonts' baselines the way the browser does).
|