sandbox_exec

在箱子裡跑一個 shell 命令:前景跑(有逾時),或背景跑(輸出寫到 log 檔)。前景命令執行時畫面有變,這一步會留截圖與錄影給人看。

輸入 輸出
id、cmd、note、cwd、timeoutSec、background、strict、user(root 或 tester)、readOnly、tailBytes、readyPort、readyLog、baseline stdout、stderr、exitCode、truncated、durationMs、timedOut、signal、boxTime、note、execId;輸出被截斷(或是二進位)時有 outputUrl、outputBytes 與 outputNote;被逾時以外的訊號結束時有 endReason;命令留下還在跑的程序時有 leftoverChildren 與 hint;往環境位址或 link 的連線接不通時有 privateEndpointErrors 與 privateEndpointHint;background 時有 bgId 與 logPath;帶 baseline 時有下面說的比較結果
  • note(必填):一句話說明這個命令要做什麼,用人讀的語言寫(工具說明裡會寫明是哪一種)。app 顯示它而不是指令原文,當作箱子目前在做的事。
  • cwd 相對於箱子的工作目錄 /work,預設就是它。
  • timeoutSec 限制前景命令:預設 600,最多 3600。時間到了,命令會被 SIGKILL 砍掉,連同它在同一個 process group 裡起的程式(用 & 起的也算),回傳到那時為止印出的輸出,timedOut 是 true、signal 是 "SIGKILL"、exitCode 是 -1。
  • 每個前景呼叫都有 execId(本機轉接器在送出呼叫前就選好)。結果回來之前呼叫就斷了的話(連線斷掉,或 client 不等了),命令不會停:它會跑到結束或 timeoutSec 用完,用 sandbox_procs 的 wait、把 bgId 設成這個 execId,就拿得到原本那次呼叫該回的東西(result:stdout、stderr、exitCode…),所以不要先重跑一次。轉接器的錯誤訊息會寫出 execId 與那個呼叫。最近 200 個前景結果會保留。用這個 execId 呼叫 stop 可以提早結束它。
  • 輸出:stdout 與 stderr 各留最後 16 KB(tailBytes 可以設少一點,1 到 16384)。印得更多時,前面會被截掉:truncated: true,outputUrl 連到完整的輸出(兩個 stream 照印出的順序;一小時有效),outputBytes 是它的大小,outputNote 說明每個 stream 各留了多少。大部分是二進位的 stream(NUL 位元組、無效的 UTF-8)不會回傳:它會顯示 [binary output: N bytes, not shown; …],完整輸出在 outputUrl;文字裡零星的 NUL 位元組會以 ␀ 或 [N NUL bytes] 回來。輸出不是終端機,所以程式會印非互動的格式:node --test 印的是 TAP(# fail 2、not ok 3 - name),不是終端機上那種 ℹ fail 行。
  • 不是逾時造成的訊號會附 endReason:stopped by sandbox_procs stop、out-of-memory killer(箱子記憶體不夠了:少同時跑幾個吃重的工作,例如 go build -p 2,或開大一點的箱子),或別的程序(kill、pkill)。
  • cmd 是一個 JSON 字串。要寫檔案,用加引號的 heredoc cat > path <<'EOF' … EOF:裡面什麼都不會展開,反斜線照原樣保留(不加引號的 <<EOF 會展開 $ 與反引號)。長檔案、很多檔案,用 sandbox_sync 送,不要塞進 cmd:超過 256 KB 的 cmd 會被拒絕,很長的 cmd 也可能讓 client 送出壞掉的參數,回來的是 arguments: …。command 也可以當作 cmd 的另一個名字。
  • readOnly: true 給只讀取的前景命令用(log、檔案、狀態):它不帶 DISPLAY 跑,什麼都不錄,有人透過 takeoverUrl 接手箱子、其他命令都得等的時候,它也照跑。它不是用來看或操作畫面的。不能和 background 一起用。
  • 前景呼叫在 shell 結束時就回,最多再等 3 秒讓還接著輸出的程序放手。用 &、nohup 或 setsid 起、那時還在跑的程序就不管了:結果有 leftoverChildren: true 與一句 hint,它們繼續跑、沒有東西追蹤它們,之後印的東西都會丟掉。如果先到 timeoutSec,它們會跟整個 process group 一起被砍掉。要一直跑的東西(dev server、watcher、資料庫、模擬器)一律用自己的 background: true 呼叫起;sandbox_procs 也只認得這種。
  • exitCode 只看最後一個命令,所以失敗的測試接到 tee、或後面還有一行,結果都是 0。strict: true 會在命令前面加 set -eo pipefail:exitCode 就是第一個失敗的命令或管線那一段的,後面的也不會再跑。
  • 測試輸出很長時,讓結果只留重要的,不要讓無關的 log 把失敗淹掉,例如 go test ./... 2>&1 | grep -E '^(--- FAIL|FAIL|ok|panic)'(加 strict: true 或 set -o pipefail,失敗才會反映在 exitCode),或 go test -json ./... | jq -r 'select(.Action=="fail") | .Package + " " + (.Test // "")'。超過 16 KB 的輸出從開頭截掉,完整的在 outputUrl。
  • 只有箱子要用的臨時測試(例如造假資料、對照 rollup 結果的 fixture)可以直接寫進箱子裡的副本、跑完就刪,本機的 repo 完全不碰:cat > /work/<repo>/internal/rollup/rollup_sbx_test.go <<'EOF' … EOF,再 go test ./internal/rollup -run Sbx,同一個命令裡把檔案 rm 掉。真的要留成正式測試時才用 sandbox_pull 帶回來。
  • user: "tester" 以非 root 使用者跑這個命令(uid 1000 空著就用 1000,在 docker 群組裡),有自己的 HOME、TMPDIR(/tmp/u<uid>,0700)、GOPATH 與 npm 快取,第一次用到時才建。測會切 uid 的程式(setpriv、每個會員一個 uid)或不肯以 root 跑的程式時用它:以 root 跑時,切到別的 uid 的子行程進不去 root 建的 0700 暫存目錄,整包測試就因此大量失敗。每次這樣跑之前,cwd 底下 root 擁有的檔案(/work/.sbx 除外)會先交給 tester,讓它寫得進去,所以 cwd 給 repo,不要給整個 /work。預設是 root。/tmp 本身是所有 uid 共用的:測試以別的 uid 跑 CLI 時,可能先替那個 uid 建好 /tmp/claude-<uid> 這類目錄,之後以那個 uid 跑的 CLI 就拒用(Temp directory /tmp/claude-1001 is owned by uid 97884)。每個 uid 給自己的暫存目錄(TMPDIR;Claude Code 是 CLAUDE_CODE_TMPDIR),已經撞到的刪掉那個目錄就好。
  • background: true 會立刻回 bgId 與 logPath;程序在呼叫結束後繼續跑,輸出寫到那個檔案 /work/.sbx/logs/<bgId>.log,用另一個 sandbox_exec 讀。sandbox_procs 列出這些命令(已結束的也在內)、等其中一個結束,或連同它起的所有程序一起停掉;sandbox_status.running.background 列出還在跑的,附 logPath。要停就用 bgId,不要用 pkill -f 或 pgrep -f。命令是從腳本檔跑的,pattern 不會再打到跑你這次 sandbox_exec 的 shell,但命令列含有它的其他程序都會被比對到,別的背景工作也算。不是用 background 起的程序,用 pkill -x <名稱> 比對完整的程序名稱,或用 kill <pid>。
    • 輸出寫到檔案或管線(而不是終端機)時,程式會先緩衝,所以 log 可能好幾分鐘都是空的,然後一次整批出現。每個命令都會設 PYTHONUNBUFFERED=1(除非你自己設了);管線裡用 sed -u、grep --line-buffered 或 stdbuf -oL <command>,每一行一印出就會進 log。Node 的 console.log 寫到檔案時會立刻寫入。
    • readyPort(server 起來後會在 127.0.0.1 或 ::1 接受連線的埠)與 readyLog(正規表示式,RE2 語法,逐行比對 log,例如 "Listening on|ready in")用來表示命令什麼時候準備好。sandbox_procs 的 wait 帶 until: "ready",只要其中一個成立就回,不必固定 sleep;等的時候也可以再設這兩個。
    • psbx-step <name> -- <command> [args...] 執行較長腳本中的一個步驟,記下它的開始、結束碼與耗時;用那個 bgId(或前景的 execId)呼叫 sandbox_procs,會把它們列成 steps,即使 log 什麼都沒寫,也看得出哪個步驟正在跑、哪個失敗了。結束碼會原樣傳回(psbx-step build -- make && psbx-step test -- make test 在第一個失敗處就停);管線要放進 bash -c '…' 裡。
  • baseline: true 把同一個命令接連跑兩次:先在 cwd,再到 <dest>-baseline 裡的同一個位置;<dest>-baseline 是 sandbox_sync 的 baseline 放在 dest 旁邊的較早的樹(cwd 必須在這樣同步的 dest 裡)。它從兩份輸出挑出失敗的測試(go test 的 --- FAIL 與 FAIL <package> 行、node --test TAP 的 not ok、node、jest 與 vitest 的 ✖、✕、× 行、pytest 的 FAILED 與 ERROR),去掉耗時與測試編號,回傳 onlyMine(只在有你的改動時失敗)、alreadyFailing(兩邊都失敗)、fixedByMine(只在改動前失敗)、counts、exitCode 與 baselineExitCode,以及 mineLog 與 baselineLog:兩份完整輸出,在 /work/.sbx/logs 底下。沒認出任何測試名稱、但有一次執行以非零結束時,會寫在 summary 裡:這時請讀 log。不能和 background 一起用。
  • 專案自己的依賴不在映像裡。Python 專案先在箱子裡 pip install -r requirements.txt(以及專案其他的 requirements*.txt)再跑測試;pip 裝到系統,break-system-packages 已經設好。箱子裡任何地方的隨手 Node 腳本,沒有 package.json 也能直接 import playwright、playwright-core 與 ws(import { chromium } from 'playwright',或 require),不用 npm install:它們和 Playwright 的 Chromium、Firefox、WebKit 整顆箱子都裝好了。這些瀏覽器是 Playwright 1.56.0 的:專案釘了別版的 @playwright/test 會報 Executable doesn't exist at /usr/local/share/playwright/…,先在專案裡 npx playwright install chromium(約 20 秒,裝在共用的瀏覽器目錄),或啟動時用 executablePath: '/usr/bin/chromium'。要用別的 Node 大版本跑:npx -y node@20 script.js(或 node@24,第一次約 5 秒)。映像裡也有 ripgrep(rg)、zip、xxd、bats、bwrap、xclip、python(Python 3.11,裝好了 Pillow、numpy、boto3、fonttools 與 brotli;3.12、3.13 用 toolchains 選)、ImageMagick(convert、montage)、en_US.UTF-8 語系與中日韓字型。git 已經設好身分(ParallelSandbox Box,在 /etc/gitconfig 與 root 的 ~/.gitconfig)與 safe.directory *,要 commit 的測試不用再設。nproc 與 free -g 看到的就是這個箱子自己的 CPU 與記憶體;箱子裡沒有 cgroup 配額(沒有 /sys/fs/cgroup/cpu.max)。箱子的 IBus 輸入法經 IBUS_ADDRESS 帶給每個命令,測試要自己起 ibus-daemon 時先 env -u IBUS_ADDRESS。映像裡有什麼列在 /etc/sbx/manifest.json。
  • 命令以 root 在 bash -l 裡跑。環境裡已經有 DISPLAY=:99(虛擬螢幕,所以你開的東西在 sandbox_shot 與 takeoverUrl 看得到)、SBX_BOX_ID(箱子 id)、SBX_SCENE_HOST(場景網址的 host,含 key,當成網址本身保管)、交給箱子的 secrets,以及箱子的環境指定了 AWS 角色時的 AWS_CONTAINER_CREDENTIALS_FULL_URI 與 AWS_REGION。容器不會自動拿到這些,要自己帶(docker run -e SBX_BOX_ID ...)。每個命令也會拿到 PYTHONUNBUFFERED=1、PSBX_JOB_ID(它的 bgId 或 execId)與 PSBX_STEPS(psbx-step 記錄的位置)。
  • 回來的東西裡密鑰的值會遮掉。stdout、stderr 與 outputUrl 那份完整輸出裡(sandbox_procs 的 cmd、logTail 與 sandbox_status 列的程序也一樣),注入箱子的 secret、以及 /work/.sbx/env 裡設定的值,8 個字以上的都換成 ****;多行的值(例如 PEM 金鑰)逐行遮。名字看得出是位址或模式的設定照樣看得到,方便除錯:HOST、PORT、ENV、TZ、LANG、STAGE,以及結尾是 _HOST、_HOSTNAME、_PORT、_REGION、_ENV、_ENVIRONMENT、_STAGE、_MODE、_LEVEL、_PATH、_DIR、_NAME、_TIMEZONE 或 _LOCALE 的名字,除非名字裡還有 SECRET、TOKEN、PASSWORD、PASSWD、PWD、KEY、CREDENTIAL、AUTH、PRIVATE、SIGNATURE、SALT、COOKIE、SESSION 或 DSN。不到 8 個字的值(true、3000、dev)不遮,其他東西也不遮,例如 psbx-testdb 的連線字串。檔案本身不改:背景命令的 log 檔裡是真的值(經 sandbox_exec 讀出來時又會遮掉),sandbox_get 與 sandbox_pull 拿到的檔案也是原樣。
  • 前景命令跑的期間(包括命令結束後、拍截圖前的那 0.6 秒),箱子螢幕(DISPLAY=:99,或接著的手機)上的畫面有變的話,這一步會自動留下一張截圖,通常還有一段錄影(例外見下面:有時只有截圖,有時什麼都不留):錄影從看到畫面變的那一刻開始(畫面每 1 秒才比一次,接著手機時每 2 秒,所以會比畫面真正變的時候晚一點),錄到拍下截圖為止;截圖在命令結束後 0.6 秒拍(這 0.6 秒內你又跑了下一個前景命令的話,就在那個命令開始前馬上拍)。畫面沒變就什麼都不留。人在 app 裡這個箱子的「截圖與錄影」看得到,每一步標著時間和你給的 note。所以瀏覽器與 App 的測試請開著畫面在 :99 上跑(Playwright 的 --headed、HEADED=1),人才看得到每一步測了什麼。每一種畫面留多久、箱子停掉之後誰還看得到,總表在收尾。細節與例外:
    • 怎麼算有變:命令開始前先記下畫面,之後每 1 秒(接著手機時每 2 秒)跟它比一次;游標閃爍不算變。命令結束後再等 0.6 秒(這 0.6 秒內下一個前景命令開始的話,只等到它開始的那一刻),到那時還沒看到變化的話,再比最後一次,有變就拍那張截圖。整段畫面都沒變(例如只跑 npm test)就什麼都不留,截圖也沒有。
    • 不到 1 秒就結束的命令(例如用 xdotool 點一下)來不及開始錄影:畫面在命令結束後 0.6 秒內變了,就只有那張截圖;更晚才變(例如點下去之後頁面要載入好幾秒)就什麼都不留。要留下點擊之後的畫面,在同一個命令裡多等幾秒,例如 xdotool mousemove 400 300 click 1; sleep 2。接著手機時要等更久:開始前的畫面是命令開始之後才抓的(iPhone 抓一張要一兩秒),抓好之後每 2 秒才比一次,所以錄影最早也要命令開始 2 秒後才開始(iPhone 抓圖慢,還會更晚),點一下(artemis-adb tap、psbx-ios tap)的變化也可能被當成開始前的畫面,什麼都不留。點擊前後都多等幾秒,例如 sleep 3; psbx-ios tap 200 400; sleep 3。
    • 這一步本身要不要錄影,只在第一次看到畫面變的那一刻決定一次,之後不再改。第一次看到畫面變時,你正用 sandbox_shot 錄影的話,命令期間畫面怎麼變,只錄在你用 sandbox_shot 開的那段錄影裡;這一步本身沒有影片(你那段錄影也不會算成這一步的),只留命令結束後拍的那張截圖,之後也不會補上影片。第一次看到畫面變時,你沒在錄的話,這一步本身照常錄影,也拍那張截圖。你用 sandbox_shot 開的錄影永遠不會併進這個命令的那一步,它的下場只有兩種:你自己用 record: "stop" 停掉的,會上傳,在 app 裡成為另外一步(kind 是 shot、detail 是 record 的那一步);被自動停掉的,不會上傳,也不會成為任何一步,app 裡永遠看不到。所以錄影被自動停掉的話,那段錄影期間跑的前景命令,人只看得到各自那一步的截圖。ParallelSandbox 自動停掉你的錄影,最早也要等到箱子最後一次使用(會動到箱子的工具呼叫結束或喚醒,見箱子狀態)起算滿 1 小時,而且要等到沒有別的東西讓箱子醒著;跟錄了多久無關,前景命令跑的期間也不會自動停掉它。被自動停掉的 mp4 留在哪、怎麼拿、拿回來之後留多久,都寫在 sandbox_shot 講 record 的那一點。
    • 一步最多錄 10 分鐘,從開始錄(第一次看到畫面變)的那一刻算起,timeoutSec 超過 600 的命令才會碰到(錄影最早在命令開始 1 秒後才開始,接著手機時更晚,所以 600 秒內結束的命令錄不滿 10 分鐘):錄滿 10 分鐘之後命令照樣跑,只是不再錄;錄好的 10 分鐘照樣留著,命令結束後照樣拍截圖。
    • background: true 的命令不錄。
    • 這些截圖與錄影拍攝後保留 7 天。箱子停掉後,app「全部沙盒」的「已停止(7 天內)」仍能打開該箱子的截圖與錄影;箱子本身和 /work 不能恢復。有設 idleTimeoutMin 時,上限是 10080 分鐘(7 天),詳見箱子狀態。
    • 箱子停掉之後你還拿得到,但只能用 REST API,沒有對應的 MCP 工具:帶 API key 呼叫 GET https://api.parallelsandbox.com/v1/boxes/{id}/media,每一步拍下後 7 天內都拿得到(最多最新的 200 步,見 REST)。要人看每一步做了什麼,就用 sandbox_review 交件,別直接 sandbox_stop(收尾)。
  • 箱子是 x86_64(amd64):跑在 Intel Xeon 主機上的 Debian 12。請拉或 build linux/amd64 的 image。只有 arm64 的 image(例如為 Graviton build 的,或在 Apple silicon 的 Mac 上沒帶 --platform linux/amd64 build 的)會失敗:exec format error。箱子裡沒有裝模擬:docker run --privileged --rm tonistiigi/binfmt --install arm64 可以替那個箱子加上,但模擬的容器很慢(我們試的箱子上,同一段 CPU 迴圈花了約 6 倍時間),所以請改 build amd64,或 push 多架構的 image(docker buildx build --platform linux/amd64,linux/arm64)。要給別處用的 arm64 產物(Graviton、arm64 的 Lambda),只要不在箱子裡跑 arm64 的東西,不裝模擬也 build 得出來:CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build,或對 RUN 步驟都在 build 平台上跑的 Dockerfile(編譯的那個 stage 寫 FROM --platform=$BUILDPLATFORM …,再 COPY --from 那個 stage)下 docker buildx build --platform linux/arm64。arm64 stage 裡的 RUN 步驟沒有模擬會失敗,build 出來的 image 也不能在箱子裡跑(exec format error)。箱子是 Debian 12、glibc 2.36:在箱子裡用 cgo 編的 Go 程式(有 C 編譯器時預設就是,箱子有)需要這個 glibc,放進較舊的執行映像會報 GLIBC_2.34 not found;用 CGO_ENABLED=0 編,或在程式要跑的同一個基底映像裡編。
  • 箱子螢幕上的瀏覽器:
    • 螢幕是 390 × 844 直向,箱子用 orientation: "landscape" 開的才是 1280 × 800(sandbox_start)。Chromium 視窗最窄 500 像素,所以直向螢幕要用 --force-device-scale-factor=0.78 --window-size=500,1082 --window-position=0,0 開才剛好鋪滿:以 background: true 跑 chromium --no-sandbox --kiosk --force-device-scale-factor=0.78 --window-size=500,1082 --window-position=0,0 --remote-debugging-port=9222 --user-data-dir=/tmp/chrome http://localhost:3000/。這時頁面的 CSS 寬度是 500。要看這個已登入頁面剛好手機寬的版面,用 sandbox_shot 的 target: "tab" 加 width(需要 --remote-debugging-port):箱子會讓那個分頁維持這個 viewport,你繼續在裡面操作也不會變回去。沒登入的頁面用 target: "url"。橫向螢幕用 --window-size=1280,800 取代那三個參數。命令以 root 跑,所以 Chromium 要帶 --no-sandbox。
    • 等條件,不要固定 sleep:timeout 60 xdotool search --sync --onlyvisible --class chromium 等到視窗出來才回;帶 --remote-debugging-port=9222 開的 Chromium,curl -s http://127.0.0.1:9222/json/version 有回應就能接 CDP;sandbox_shot 自己也會等(waitFor、stableMs;target: "tab" 會等還在開的瀏覽器最多 45 秒)。
    • 視窗由 openbox 管:xdotool windowactivate 與 getactivewindow 能用,新開的視窗會拿到鍵盤焦點,對話框(Electron 的 dialog.showMessageBox、GTK)置中出現、有標題列。其他視窗不加框,所以從 0,0 開、跟螢幕一樣大的視窗照樣剛好鋪滿;openbox 也沒有自己的快捷鍵,每個按鍵都送到程式。滑鼠指標平常停在右下角、不在畫面裡,測 hover 時自己移過去。DBUS_SESSION_BUS_ADDRESS 指向這個螢幕的 session bus,桌面通知(Electron 的 Notification、notify-send)會出現在右上角。
    • 螢幕只有這兩種大小。要看別的尺寸的網頁,用 sandbox_shot 的 target: "url" 或 "tab" 加 width、height。桌面 App 要例如 1440 × 900,自己開一個螢幕(以 background: true 跑 Xvfb :100 -screen 0 1440x900x24),用 DISPLAY=:100 跑 App,再用 ffmpeg -f x11grab -video_size 1440x900 -i :100 -frames:v 1 shot.png 拍、sandbox_get 拿回來:sandbox_shot 與人看到的畫面都只有 :99。
    • 用角色與無障礙名稱找元素(page.getByRole('button', { name: 'Save' }),icon 按鈕的 aria-label 也找得到),不要用畫面上的文字或位置:只有 icon 的按鈕沒有文字,選到好幾個元素的 locator 會撞上 Playwright 的 strict mode。
    • 一個瀏覽器一次只驅動一個分頁。不在最前面的分頁拿不到畫格,對它 page.screenshot 會逾時;同一個瀏覽器換分頁操作前先 page.bringToFront()(CDP 是 Page.bringToFront),同時跑的腳本各開一個瀏覽器,各用自己的 --user-data-dir 與除錯埠。
    • 需要使用者手勢的動作(window.open、剪貼簿、全螢幕)要真實輸入:Playwright 的 click()、CDP 的 Input.dispatchMouseEvent 或 xdotool click。經 CDP Runtime.evaluate 送的 element.click() 不算使用者操作(除非呼叫帶 userGesture: true),它開的彈出視窗會被擋掉,而且不報錯。
    • 給自己的 CDP 腳本用的最小有畫面 Chromium:用 background: true 跑 chromium --no-sandbox --remote-debugging-port=9222 --user-data-dir=/tmp/chrome <網址>(以 root 跑,沒有 --no-sandbox 會直接結束),用 until curl -sf http://127.0.0.1:9222/json/version >/dev/null; do sleep 0.2; done 等它起來,再接上去:Playwright 的 chromium.connectOverCDP('http://127.0.0.1:9222'),或 /json/version 回的 webSocketDebuggerUrl。
    • 程式不結束視窗就一直在:用 background: true 開、用 sandbox_procs 的 stop 收;sandbox_shot 的 target: "window" 會在 windows 列出螢幕上還有哪些視窗(任何一個都能用 xdotool windowkill <id> 關)。
    • 加了 --force-device-scale-factor 時,--window-size 是縮放前的點數,而且無條件捨去:2 倍時要鋪滿 1280 × 800 的螢幕寫 --window-size=641,401;寫 640,400 右邊與下面會露出 1 像素黑邊,寫 1280,800 等於要一個兩倍螢幕大的視窗。
    • 箱子沒有 GPU,WebGL 預設不能用(Chromium 的 log 有 WebGL1 blocklisted,網頁偵測不到 WebGL)。需要的頁面加 --use-angle=swiftshader --enable-unsafe-swiftshader:軟體算繪,結果正確但慢。
    • 語音輸入:/opt/psbx/fake-mic/en.wav 與 /opt/psbx/fake-mic/zh.wav 各是一句英文與中文的人聲。Chromium 帶 --use-fake-ui-for-media-stream --use-fake-device-for-media-stream --use-file-for-fake-audio-capture=/opt/psbx/fake-mic/zh.wav 開,getUserMedia 拿到的麥克風就是這段錄音,一直循環,也不跳權限詢問。要別的句子用 espeak-ng -v en-us -w /tmp/say.wav "..."(中文用 -v cmn-latn-pinyin;直接用 cmn 會把拼音的聲調數字用英文唸出來)。
    • 在模擬手機的頁面上滑動,送 CDP 的 Input.dispatchTouchEvent:touchStart、幾個 touchMove、最後 touchEnd。Input.synthesizeScrollGesture 帶 gestureSourceType: "touch" 在箱子裡捲不動。
    • 箱子的 Chromium 讓 localhost、127.0.0.1、*.localhost 與 *.parallelsandbox.com 不用問就能用剪貼簿,開別種語言的頁面也不跳翻譯提示,不會蓋在截圖與錄影上(managed policy,/etc/chromium/policies/managed/psbx.json)。navigator.clipboard.readText() 仍然要頁面有焦點,先把它帶到最前面。其他 origin 要先授權(Playwright:context.grantPermissions(['clipboard-read', 'clipboard-write']);CDP 的 Browser.grantPermissions 只在那條連線開著時有效);沒有授權時,Chromium 會在頁面上方跳出權限列,呼叫就一直等著。
    • 要確認頁面實際用了哪個字型,問 Chromium:對那個元素用 CDP CSS.getPlatformFontsForNode,會列出用到的字型與各畫了幾個字。依 unicode-range 分片的網路字型(多數 CJK 字型與 Google Fonts)用 document.fonts.check() 不可靠:已經用網路字型畫好的字也可能回 false。
    • Chrome 擴充功能(Manifest V3)用 --load-extension=/work/<ext> --disable-extensions-except=/work/<ext> 與專用的 --user-data-dir 載入;Playwright 要用 chromium.launchPersistentContext 帶這些 args、headless: false(在 DISPLAY=:99 上)。chrome.runtime.reload() 等開發者操作要在那個設定檔開啟開發人員模式(chrome://extensions),不然擴充功能會以原因 16777216 被停用。它的 service worker 是 CDP 裡 type 為 service_worker 的 target;要重啟就從 chrome://extensions 重新載入擴充功能,或關掉那個 target。下載不要動:Playwright 的 acceptDownloads 與 CDP Browser.setDownloadBehavior 設 allow 會在擴充功能的 chrome.downloads.onDeterminingFilename 之前就把下載處理掉,測這類行為要用預設行為。
  • 箱子裡的程式連不到環境位址或 link 時,它只看得到連線被 reset 或關掉。這時結果會帶 privateEndpointErrors([{target, error, count}],這個命令期間的每一個失敗)與 privateEndpointHint,寫著連接器或 ParallelSandbox 說的原因,例如 the connector could not reach chatbot-dev.svc.local:8000: lookup chatbot-dev.svc.local: no such host(那個服務在他們網路裡沒有在跑的 task)或 the connector "office" of this environment is offline。這些在箱子的另一頭,改程式修不好(環境)。既不是環境位址、也沒宣告的名字會去公用 DNS 查:看起來是公開網域卻出現 ERR_NAME_NOT_RESOLVED 或 NXDOMAIN,表示公用 DNS 沒有它的紀錄,先在箱子裡 dig <名字> 確認,再懷疑箱子。
  • 有人透過 takeoverUrl 連著(或在箱子畫面上試 box 的驗收)時,sandbox_exec 不執行,回錯誤(HTTP 423),除非命令是 readOnly:錯誤說人從什麼時候開始用、為什麼、最晚什麼時候還回來。等一下再試;這段期間 sandbox_get、sandbox_procs 照樣能用,sandbox_shot 也照樣拍得到畫面,要繼續改程式也可以開另一個箱子把程式同步過去。

所有工具與主題都列在工具參考。