20 KiB
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 endpointwss://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)、findBannedPlumbingTermregex。- 語音代理自己的工具含
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 maxResults200;ripgrep timeoutMs20000。 - 配額預算(不是 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. 方法與信心度說明
- 純靜態。未執行目標程式任何部分 → 本文無一條「執行過」的斷言。
- 缺席結論全部限定範圍:
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:僅檔名,非行為證據。
- asar 目錄清單只跑到 offset 560/1027,
dist/electron-main/main-app.cjs的拼法未在清單直接讀到(前 560 筆被 locales/menubar-tray PNG 與 renderer chunk 吃滿)→ 若要確認,用offset=560分頁。 kind: unpacked的檔案不在 asar 內,實體在Contents/Resources/app.asar.unpacked/<相對路徑>。- evidence id 會過期(綁定產生它的查詢週期)。因此本報告一律引 finding id(
fnd_1…fnd_12、fnd_14、fnd_15),其 evidence 連結已入庫綁定。 - 重建對應物(
app/src/)中的每一個 token 數字都是作者設定,唯一源自 Grok Bot 的引數是client_speculative_summarization_config的0.70/0.05與一組 byte/char 上限。詳見GROK_BOT_TOKENS.md§11。