AnyBot/GROK_BOT_FEATURES.md

229 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Grok Bot(內部代號 `sand`)功能分析
**分析目標**:`/Applications/Grok Bot.app`(active target `t_a7c7c1454fc1`)
**asar**:`/Applications/Grok Bot.app/Contents/Resources/app.asar`(1027 entries,header 243890 B,integrity verified)
**SHA-256**:`a7c7c1454fc1a9169eca436fe6cf6fdf363e2c359da7312c49c22945170e36fd`
**外殼**:Electron 40.1.0,`package.json` name = `"sand"`,`type: module`,main = `dist/electron-main/main.cjs`
**方法**:純靜態(asar 目錄清單、字串/符號掃描、內建反編譯器、`js_app_map` 應用圖)。**未執行目標程式**,因此本文所有結論都是「程式碼/資源中存在」的證據,不是「執行時發生了」的證據。asar 內無 source map。
**標記**
- **觀察**:直接讀到(檔案、符號名、字串位址、設定值)。
- **推論(derived)**:由觀察算出/歸納出。
- **推論(inferred)**:機制作最佳解釋,未直接讀到。
- **推測(estimated)**:best guess,證據薄弱。
**引用規則**:每條斷言引 `fnd_*`(已入庫 finding,綁定原始 evidence)。本輪**未取到新的 evidence id**,所以不列 `ev_*`。
---
## 1. 摘要
| # | 結論 | 標記 | 依據 |
|---|---|---|---|
| 1 | Grok Bot 是 Anysphere(Cursor)私有 monorepo 建置出來的 Electron 應用,約 50 個 `@anysphere/*` + `@sand/*` workspace package,包含 `@anysphere/grok-bot`、`agent-*`、`chat-inference`、`mcp-core`、`xai-commerce-prepaid` | 觀察 | `fnd_1` |
| 2 | **文字代理迴圈不在桌面端**。它是一個連到遠端 gateway / "box" 的傳輸客戶端;模型呼叫、prompt 組裝、工具編排都在伺服器 | 推論(derived) | `fnd_5`, `fnd_6` |
| 3 | 桌面端真正的「執行手」是獨立 Node 程序 `dist/local-exec-daemon/main.cjs`:由遠端代理反向驱动的本地工具執行器(搜尋/讀檔/shell/電腦操作/MCP/macOS 內建 App) | 推論(derived) | `fnd_7` |
| 4 | 用戶端有一整套分層權限:讀/寫/shell/MCP 四條 `shouldBlock*` 閘門 + allowlist / denylist / glob + 逐 app 同意 + 持久「always allow」授權 | 推論(derived) | `fnd_8`, `fnd_12` |
| 5 | 用戶端**沒有 token 計數器**;token 記帳與 context 壓縮摘要都來自伺服器串流事件。唯一在客戶端的 token 估計是字元數除以 4(語音逐字稿) | 推論(derived) | `fnd_9` + 本報告 §10 |
| 6 | 唯一在客戶端組裝 prompt 的場景是**語音通話 harness**(`wss://api.x.ai/` 是原檔字面字串):語音模型被設計成薄前端,把工作派給主代理,且不得宣稱任務已完成 | 觀察 | `fnd_10` |
| 7 | 後端是 Anysphere 自有服務(`api2.cursor.sh`,connect-rpc + protobuf;遥測 `api3.cursor.sh`;登入/計費在 cursor.com),**不是**公開 LLM API;金鑰用 Electron `safeStorage` | 觀察 | `fnd_3` |
| 8 | 電腦操作(滑鼠拖曳、鍵盤輸入)的實際輸入合成落在一個原生 macOS helper `Contents/Helpers/CUGrokBotService.app`(Swift module `CUCore`),每次 drag 需自帶一個 permit | 推論(inferred) | `fnd_14` |
| 9 | 模型清單由伺服器提供;程式碼中出現的模型名是 `grok-4.5`(預設壓縮模型)、`grok-4.6`(model picker 實驗) | 觀察 | `fnd_9` + `unk_dd4ee2d6bc4685b5`(已解) |
---
## 2. 程序架構
**觀察**(`fnd_2`):
- 主程序拆成兩段:`dist/electron-main/main-core.cjs` 與 `dist/electron-main/main-app.cjs`。
- preload 有兩支:`preload.cjs`、`preload-avatar-source.cjs`。
- 兩個獨立 Node 程序:
- `dist/local-exec-daemon/main.cjs` — 沙箱化本地命令執行 + 檔案系統 allow-list(見 §4)。
- `dist/node-agent-coordinator/main.cjs` — 代理協調器傳輸客戶端(見 §3)。
- Renderer 與 main 透過 **MessagePort** 連通:main `handle("sand:coordinator-port-request")` → 回傳 `"sand:coordinator-port"`;另有 boot-gate / renderer-alive / network-online 信號。
- 共 8 處 window 構造,多數 `contextIsolation` + `sandbox`,其中 2 處開了 `webviewTag`。
- 自訂協議:`sand-mcp-app://`、`sand-visualization://`、`sand-media://`、`sand-html-preview://`、`sand-avatar://`、`sand-favicon://`。
**推論(derived)**:這種「main 只當殼 + 兩個独立 Node 程序做重活」的切法,是把「長生命週期的代理/session 狀態」從 Electron 生命週期裡拆出來,避免 renderer 重載或 window 關閉中斷 session。
---
## 3. 代理迴圈與後端
**觀察**(`fnd_5`)— `dist/node-agent-coordinator/main.cjs` 是通往遠端 gateway / box 的傳輸客戶端,符號包括:
`createAgent` / `createAgentWithRetry`、`sendPrompt`(`POST {base}/api/sendPrompt`)、`streamEvents` / `runEventLoop`(`GET {base}/events`)、`postEcho`、`listAgents`、`openAgentTail` / `getAgentTranscriptTail`、`transcribeAudio`、`ensureForeverBox` / `noteBoxAsleep` / `requestBoxIntent`,以及一條 **WebAuthn 同意通道**(`/webauthn/requests`、`/webauthn/responses`、`requestConsent`、`onPinRequest`)。
它向 UI 發射的主題:automations、workflows、subagents、skills、secrets、tasks、meetings、library docs、computerActions、`onTurnFailed`。
**觀察**(`fnd_6`)— main 端通往 box 的路徑:`probeGateway` → `{gatewayUrl}/health`、`flushBoxStore` → `{gatewayUrl}/api/snapshotBoxStoreNow`、`issueBoxCredential`(POST)。`main-app.cjs` 與 `main-core.cjs` 中**未見**直接 fetch 到任何 LLM inference API。
**推論(derived)**:LLM 推理由伺服器端代理發起,桌面端只送 prompt、收事件流。
**範圍限制(必讀)**:`main-app.cjs` 有 **18 處動態 fetch 點**,connect-rpc transport 未完全解析 → 「未見直接呼叫」只適用於我解析到的路徑,不等於不exists。
**Box 供應**(`fnd_11`,推論(derived)):正式路徑 `<class>.ensureBox` → `client.ensureSandBox`(connect-rpc,帶 rate-limit retry-after、parkMode、access-hold 拒絕分類)。另有一條 **dev-box** 路徑在本地跑 Docker:`imageExists`(`docker image inspect`)、`pullImage`(`docker pull`)、`removeContainer`(`docker rm -f` + `recreate-intent.json`)、`resetBox`、`recreateContainer`、`wipeSandData`。`issueBoxCredential` POST 一枚 `local-exec` 憑證,帶 `x-cursor-team-id` 與 `x-ghost-mode` header → **推測**:讓 local-exec daemon 對 box 認證。
---
## 4. 本地工具執行器(`dist/local-exec-daemon/main.cjs`)
**觀察 + 推論(derived)**(`fnd_7`):它**反向撥出**連回 gateway 接指令:`redial`、`streamRequests`、`handleRequest`、`handleExecRequest`、`handleCancel`、`postBatch`、`upload` / `download`。
實作的工具族:
| 工具族 | 觀察到的细节 | 標記 |
|---|---|---|
| 檔案搜尋(ripgrep) | `files` / `count` / `content` 三種模式、timeout、blocked-file 過濾 | 觀察 |
| 目錄樹 | 目錄 tree 輸出 | 觀察 |
| Shell | 執行 + **背景交接**:`requestBackground`、`forceBackgroundByToolCallId`、`timedOut`、`readTerminalFile` | 觀察 |
| 電腦操作 | 經 sidecar helper 走 **unix socket**:`screenshot`、`click`、`scroll`、`move`、`drag`、`typeText`、`pressKey`、`startControl`/`releaseControl`、座標換算 `apiToDisplay`/`displayToApi`、ffmpeg 動作錄製 | 觀察 |
| MCP client | 含 OAuth token 存放 | 觀察 |
| macOS App 工具 | Reminders、Calendar(CRUD)、Contacts(search)、Mail(`composeSend` / `sentCheck`)、Messages(操作),逐 app 同意 + 冪等送出(`rememberSend`、`replayedSend`、`withSendKey`) | 觀察 |
| 上下文載入 | 載入 Cursor rules 與 agent skills | 觀察 |
| 規則解析 | 存在 `tree-sitter-bash.node`(darwin-arm64 prebuild)+ `web-tree-sitter/tree-sitter.wasm` | 觀察 |
| shell 指令解析 | 推測:shell 指令會被解析成子指令後才做 allowlist 檢查 | **推測(estimated)** |
---
## 5. 權限模型
**觀察**(`fnd_8`)— 閘門與清單:
`shouldBlockRead`、`shouldBlockWrite`、`shouldBlockShellCommand`、`shouldBlockMcp`、`isShellCommandFullyAllowlisted`、`isMcpFullyAllowlisted`、`isWebFetchFullyAllowlisted`、`addToAllowList` / `addToDenyList`、`shouldEnforceShellInvariantBlocks`,加上 `cursorignore` / `gitignore` / repo-block 的 glob 過濾。
审批回呼:`onNeedsApproval`、`onApprovalRequested`、`onUserRejected`、`onPermissionDenied`。
**觀察**(`fnd_12`)— 客戶端擁有並推送到 box 的設定與授權:
- `recordLocalToolApproval` 是**機器範圍**的;**拒絕** send-mail,除非經 `recordMacMailSendApproval`(從草稿卡片)。
- `recordLocalToolStandingGrant` 持久化「always allow」。
- `getLocalToolPermissionCeiling`(權限上限)、`listComputerUseAllowedApps`(從 standing grants 推導)。
- MCP 自訂指令:`getMcpCustomInstructions`、`applyCustomInstructionsToMcpResult`。
- auto-review 指令與 `syncHostSettingsToBox` 同步;`pushBoxSecrets`、template skills、user bot memories。
- 遥測:`reportTurnClientOutcome`、`reportRenderTtfr`。
**推論(derived)**:權限分三層 —(1)全域 deny/glob(檔名與指令層面的硬駔)→(2)allowlist(完全放行)→(3)逐次审批 + standing grant(使用者互動)。`shouldBlock*` 是閘門,`is*FullyAllowlisted` 是「整條指令都允許才放行」的那種全量檢查。
---
## 6. 語音通話代理(客戶端唯一組裝 prompt 的地方)
**觀察**(`fnd_10`):
- `requestVoiceCallHarness` / `voiceCallHarnessSession` / `voiceCallHarnessTool` 經 `cursorAccount`;realtime endpoint `wss://api.x.ai/` 是原檔第 1116 行的**字面字串**。
- `e.render`(第 65 行)用 section builder 組出分段 system prompt:`Role & Persona`、`Instruction and tools`、`Conversation Flow`、`While work is in flight`、`When they talk over you`、`Never claim it is done`、`Voice`、`CRITICAL INSTRUCTIONS`;builder 有 `voiceLoopLines`、`noInventedWorkLines`、`rosterPromptSection`(`## Project workers`)、`findBannedPlumbingTerm` regex。
- 語音代理自己的工具含 `sendTask`、`staySilent`、`endTheCall`;`earnsASpokenTurn` 用 `SPOKEN_KINDS` 決定何時可以說話。
**設計取捨(推論 derived)**:語音模型被刻意做成**薄前端** — 只負責接話、派工(`sendTask`)、保持安靜(`staySilent`)、結束通話(`endTheCall`);**不得**在沒有結果時宣稱完成。把「不確定性」與「延遲」交给主代理,避免語音模型自編進度。
---
## 7. 整合、連線器與後端服務
**觀察**(`fnd_3`):
- 後端:`api2.cursor.sh`(`@connectrpc/connect` + protobuf),遥測 `api3.cursor.sh`,登入/計費/onboarding 在 `cursor.com`。
- 整合:`x.com` / `grok.com` 的 OAuth 連線器;Google Workspace scopes(Gmail / Drive / Calendar / Docs / Sheets / Slides / BigQuery);MCP OAuth;Statsig feature flags;Sentry。
- 祕密儲存:Electron `safeStorage`。
**觀察**(`fnd_4`)— renderer 技術堆疊(來自 deps):React + `@tanstack/react-query`、StyleX、`@base-ui/react`、TipTap(含 mentions)、`react-markdown` + `remark-gfm` + KaTeX、`motion`、`dnd-kit`、Lingui i18n;檔案進件 `pdfjs-dist` / `mammoth`(docx)/ `xlsx`;tree-sitter(bash)做指令解析。原生 macOS addons:`window-capture`、`card-text-recognition`、`devicecheck`、`notification-settings`、`address-lookup`、`proclist`、`jpeg-napi`。
**推論(derived)**:Google Workspace scope + Mail/Messages/Calendar 工具 + 「逐 app 同意」顯示這個代理被設計為**直接操作使用者的個人工作環境**,而不只在 repo 內寫程式。
---
## 8. 設定與功能目錄(UI 層線索)
**依據 A:專案內既有檔案 `app/ORIGINAL_FEATURES.md`**(5491 B、33 個標題:27 個設定分區、per-bot tabs、routine triggers、SAND‑E 的 9 domains / 108 codes)。這是專案自帶的觀察記錄,**不是**本輪分析產出的 evidence id。
**依據 B(`fnd_15`,觀察,名稱層)**:asar 的 renderer bundle 以 per-feature chunk 分割,**檔名本身就列出已出貨的 UI 功能**。`dist/renderer/assets/` 中可見:
| chunk 檔名 | 對應功能線索 |
|---|---|
| `chunk-agents-*`(十餘個變體,22–52 KB) | agents 主畫面/代理清單 |
| `chunk-agent-avatar-editor` | 代理頭像編輯 |
| `chunk-agent-invite-dialog` | 邀請代理 |
| `chunk-bot-configure-dialog` | Bot 設定對話框 |
| `chunk-bot-manage-screen`(67962 B)、`-manage-knowledge`、`-manage-section` | Bot 管理畫面 + 知識管理 |
| `chunk-bot-share-control`、`-invite-popover`、`-popover`、`-state-line`、`-trigger` | Bot 分享/邀請/狀態列/觸發器 |
| `chunk-automation-detail-panel` | 自動化詳情面板 |
| `chunk-auto-review-rule-notice`、`chunk-auto-review-selector` | auto-review 規則提示與選擇器 |
| `chunk-bonfire-dialog` | bonfire 對話框 |
| `chunk-app-copy` | 整包複製/打包 |
(來源:asar 目錄清單 offset 360,200/1027 筆,`integrity: verified`。)
**標記**:以上整章屬**觀察(名稱層)** — 檔名是觀察,功能行為**不是**由檔名證明的。
---
## 9. 原生 computer-use helper
**推論(inferred)**(`fnd_14`):輸入合成在原生 macOS helper `Contents/Helpers/CUGrokBotService.app`(Mach-O fat,Swift module `CUCore`,sha256 `1004a8cbd6a03567e2775e3cb1ec69d15d06c66d44afd7f7b627a6fad2cbab13`)。
觀察到的符號(內建反編譯器,非 Ghidra):
- `CURemoteToolService.handleDrag(...permit:)` → `CURemoteEventPoster.drag(path:[CGPoint], button:CUMouseButton, steps:Int, holdSeconds:Double, flags:CGEventFlags, permit:CUScrollControlPermit?)`;`drag` 只有 **1 個 caller** → permit 屬**逐請求授權**。
- 文字輸入走 `CUTypedCharacters.utf16Chunks` + `postChunks(_:source:CGEventSource, prepare:, delivery:_(CGEvent, isCommand:))`。
- `CUActuator.typeCharacters(pid, text, processIdentity)`、`CUActuator.click(pid, screenPoint, windowBounds, windowID, button, count, holdSeconds, processIdentity)`。
**範圍限制**:Ghidra decompiler 無結果,偽碼全來自內建反編譯器;掃描 coverage partial(120/616 函式、80/99 字串);**哪個請求欄位決定 `CUScrollControlPermit` 未解**。
---
## 10. Token 計數與 context window
完整展開見同目錄 `GROK_BOT_TOKENS.md`。此處只列結論。
**觀察(限定範圍的缺席)**:對 `dist/electron-main/main-app.cjs` 掃 `usage`、`prompt_tokens`、`completion_tokens`、`total_tokens`、`token_count`、`context_window`、`max_tokens`、`tokens_used`、`usage_ratio` → **0 命中**;符號索引含 "token" 的 147 筆全為憑證/認證字串。**→ Grok Bot 用戶端沒有 token 計數器。**(僅該檔、僅這些欄位名。)
**觀察**(真實在客戶端的东西):
- 壓縮設定:`client_speculative_summarization_config = {enabled:true, thresholdRatio:0.70, toleranceRatio:0.05, modelId:"grok-4.5"}`;閘 `speculativeSummarizationClientGates:{enabled, modelTier:"default", contextRatio:0.70}`。
- **byte 級**上限(不是 token):`summarizationInputTokenLimit: 360000`(註解 "bytes (ASCII=1 token)")、`summarizationInputByteLimit: 360000`(預設 300000)、`summarizationInputBytes: 120000`、`summarizationInputTokenLimitCap`(0=停用)、`summarizationOutputCap: 6000`、`MAX_SUMMARY_BYTES = 100*1024`、`summarizeMessages`。
- 每工具輸出上限:`read_file maxBytes` 預設 131072;`readTerminalFile maxBytes` 預設 65536;`ripgrep maxResults` 200;`ripgrep timeoutMs` 20000。
- 配額預算(**不是** context window):`token_budget = 100000`、`token_budget_per_user = 30000`、`token_budget_per_user_7d = 100000`、`token_budget_per_team`、`usage_warning_threshold = 0.8`、`usage_limit_floored`。
- Provider 錯誤分類含 `provider_413_context_too_large`。
**推論(derived)**:
- 客戶端**唯一**的 token 估計器是 `voiceCallTranscriptCharsPerToken = 4`(預設 4.0),用法 `voiceCallTranscriptTokens = ceil(charLen / charsPerToken)`,對 `voiceCallTranscriptTokenBudget = 12000` 檢查。`estimateMessagesTokenCount` 存在(main-app.cjs 約 @2743065)但算術不可見。
- `compactionSummary` 來自**伺服器**串流(`compactionSummaryFromEvent`、`applyCompactionSummaryToTimeline`、`applyCompactionSummaryFromEvent`),配 `turnUsageBreakdownFromEvent`、`reportUsage`、`usageFromEvent`、`usageLabel`、`onUsage`、`usageLabelForTurn` → **token 記帳在伺服器端**(與 `fnd_5`/`fnd_6` 一致)。
---
## 11. 模型與錯誤分類
**觀察**(`fnd_9` + `unk_dd4ee2d6bc4685b5` 已解):
- 客戶端不選模型:`SAND_DEFAULT_MODEL_ID` 只有 getter(值不可見);`getComputerUseModel` 是用戶設定;字串群 `Cursor Models` / `Includes Cursor Grok and Composer` / `Other Models`。
- 程式碼中的模型名:`Wp`(main-core.cjs)含 **`grok-4.5`**;`model_picker_experiments` 含 **`grok-4.6`**。**模型清單由伺服器提供。**
- 沒有可見的 model id 字面值(字串表被截斷)。
- Provider 錯誤 taxonomy:`provider_400_invalid_request`、`_401_auth_rejected`、`_404_not_found`、`_413_context_too_large`、`_429_quota_exceeded`、`_4xx_rejected`、`_5xx_terminal`、`_image_rejected`、`_user_blocked`、`NonRetryableProviderError`。
- `main-app.cjs` 內**沒有**文字代理迴圈、聊天 LLM call、context 壓縮客戶端碼(限定範圍缺席)。
**推論(derived)**:錯誤 taxonomy 用 HTTP status 分「可重試 / 终止」兩類(`_4xx_rejected` vs `_429_quota_exceeded` vs `_5xx_terminal`),並在客戶端做 retry 決策;`_413_context_too_large` 與 `_image_rejected` 是「把請求重新組裝後重試」的訊號,不是終止訊號。
---
## 12. 未解問題
| id | 問題 | 嚴重度 | 狀態 |
|---|---|---|---|
| `unk_67f6ca825e8ffa3f` | 伺服器端 prompt/模型路由/工具 schema 不在 asar 內;唯一路徑是執行期抓取 box 串流(需使用者核准) | high | blocked |
| `unk_70f57b409ea35f76` | 回合開始/steering/idle 語義已定案(main-app.cjs @3100283 `P3n`;coordinator `wl`/`k6`、`ZF`、`boxHandoff`);開放項是 `api2.cursor.sh` 的 wire schema | high | open |
| `unk_8b8ec93d6245e822` | 側欄 280 px 預設/拖拽 240–400/<210 px 轉 88 px rail/>220 px 展開(index.eager-app @912025 `FT`、@914394 `sKe`、@969523 `QJe`);chat 最小 424 px(`--sand-chat-min-width`);bubble 36/18 px;chat 最大寬 720 px 屬**推測** | medium | open |
| `unk_25d3b20c74d75860` | 深色配色由執行期產生(主執行檔 `Grok Bot`,sha256 `df5b07deaa0b9bd42cf63ca2f2478ac629e4fa257324047cdfe8f3ddce4d6684`,對 `font\|pingfang\|hiragino\|width\|height\|minsize\|title` 0 字串命中);重建用 `mechanics.json` 估計配色 | low | blocked |
| (fnd_14 衍生) | 哪個請求欄位決定 `CUScrollControlPermit` | medium | open |
| (工具層) | application toolset 17 個、projects toolset 27 個工具的確切名稱未逐一確認 | low | open |
| `unk_89a8fde07df81e50` | 功能清單 | — | **resolved**(`app/ORIGINAL_FEATURES.md`) |
| `unk_dd4ee2d6bc4685b5` | 模型清單來源 | — | **resolved**(伺服器提供;程式碼名 `grok-4.5`/`grok-4.6`) |
---
## 13. 方法與信心度說明
1. **純靜態**。未執行目標程式任何部分 → 本文無一條「執行過」的斷言。
2. **缺席結論全部限定範圍**:
- `fnd_9`:字串表被截斷。
- `fnd_6`:18 處動態 fetch 點未解析;connect-rpc transport 未完全解析。
- `fnd_14`:Ghidra decompiler 拒絕,偽碼全來自內建反編譯器;coverage partial(120/616 函式、80/99 字串)。
- §10 的 token 欄位掃描:僅 `dist/electron-main/main.cjs`… 精確說,僅 `dist/electron-main/main-app.cjs`,僅列出的 9 個欄位名。
- `js_app_map`:受 25000 節點上限(74905 個 node 被丟)與 `paged_modules` 限制(coverage partial 20/697 modules 逐頁取)。
- `fnd_15`:**僅檔名**,非行為證據。
3. **asar 目錄清單**只跑到 offset 560/1027,`dist/electron-main/main-app.cjs` 的拼法未在清單直接讀到(前 560 筆被 locales/menubar-tray PNG 與 renderer chunk 吃滿)→ 若要確認,用 `offset=560` 分頁。
4. **`kind: unpacked` 的檔案不在 asar 內**,實體在 `Contents/Resources/app.asar.unpacked/<相對路徑>`。
5. **evidence id 會過期**(綁定產生它的查詢週期)。因此本報告一律引 **finding id**(`fnd_1`…`fnd_12`、`fnd_14`、`fnd_15`),其 evidence 連結已入庫綁定。
6. **重建對應物(`app/src/`)中的每一個 token 數字都是作者設定**,唯一源自 Grok Bot 的引數是 `client_speculative_summarization_config` 的 `0.70` / `0.05` 與一組 byte/char 上限。詳見 `GROK_BOT_TOKENS.md` §11。