本機重現與除錯
沙盒執行你的程式時只做一件事:在你的環境映像裡,以 /bin/sh 跑一個固定 session:
. /workspace/startup.sh && . /workspace/driver.sh三個 UI 欄位落地成三個固定檔名(上傳檔案的原始檔名不會保留):
| 欄位 | 落地路徑 | 誰讀它 |
|---|---|---|
| 前置腳本(startup_script) | /workspace/startup.sh | 永遠是 sh |
| 執行指令(work_command) | /workspace/driver.sh | 永遠是 sh(空白時內容是 . /workspace/work.sh) |
| 工作腳本(work_script) | /workspace/work.sh | 看 driver 寫什麼——填了 python3 /workspace/work.sh 它就是 Python |
其餘的每次執行輸入:input JSON 會在腳本啟動前寫到 $TEAMSYNC_INPUTS_FILE 指的檔案;結果寫到 $TEAMSYNC_OUTPUT_FILE 指的路徑;之後想下載的檔案放到 $TEAMSYNC_ARTIFACTS_DIR(/workspace/artifacts,runner ≥ #1152);secret slot 以環境變數匯出。cwd 名義上是 /workspace,但契約不保證——腳本與 work_command 一律寫絕對路徑。
本機 harness
自訂環境的 Dockerfile 是你自己寫的,所以你可以在本機 build 出同一個 base,然後用同一份契約跑:
# 1) build 你上傳給環境的同一份封存
docker build -t my-env ./env-context
# 2) 組出 /workspace,跟平台落地的一模一樣
mkdir -p ws
cp startup.sh ws/startup.sh # 沒有就放空檔
cp my-work.py ws/work.sh # 注意:固定叫 work.sh,不管原本副檔名
printf '%s' 'python3 /workspace/work.sh' > ws/driver.sh # = 你的 work_command
echo '{"demo": 1}' > ws/inputs.json
# 3) 用同一個 session 跑(--network=none 是模擬 runtime_egress_enabled=false 的公司;
# 預設部署執行期可對外連線——要一致就把這個旗標拿掉)
docker run --rm --network=none \
-v "$PWD/ws":/workspace \
-e TEAMSYNC_INPUTS_FILE=/workspace/inputs.json \
-e TEAMSYNC_OUTPUT_FILE=/workspace/output.json \
-e TEAMSYNC_ARTIFACTS_DIR=/workspace/artifacts \
-e MY_SECRET_SLOT=dev-value \
my-env \
/bin/sh -c '. /workspace/startup.sh && . /workspace/driver.sh'
cat ws/output.json路徑錯、套件沒裝到、runtime 版本不對、node_modules 解析失敗——這些在本機各花五秒就能排除,不必每次都走一次「上傳 → 建置 → ready → 執行」的迴圈。
本機 harness 與真沙盒的差異
| 面向 | 本機 harness | 真沙盒 |
|---|---|---|
| Base 映像 | 你 docker build 的那份 | 同一份封存建的映像 + 平台簽章 runner layer(只加 teamsync-runner/ 底下的檔案,不動你的內容) |
| 網路 | 只有模擬 runtime_egress_enabled=false 時才加 --network=none | 預設可對外連線(runtime_egress_enabled,GET /settings),會計量;私有/loopback/metadata 網段沒有被過濾 |
| 隔離 | 一般容器 | gVisor 沙盒、非 root、資源上限(依 resource_profile) |
| 環境變數 | 你的 -e 參數加上映像自己的 ENV | 只有 PATH、ordinary_env、secret slot 與三個 TEAMSYNC_* 路徑——映像的其他 ENV(JAVA_HOME、LANG……)不會繼承(地端自 v5.10.11 起也一樣) |
| 行程模型 | /bin/sh 是容器的 PID 1;id -u 是映像的 USER | 託管雲端開啟對外網路、且 runner release 以 v5.10.11 原始碼建置的 run 會嘗試巢狀 user+PID 命名空間;主機允許私有 /proc 設定時,/bin/sh 是該命名空間的 PID 1,否則 runner 會記錄或使用直接 shell fallback。地端使用 Docker worker 的直接 /bin/sh;runner 會保留 /workspace/.teamsync/ |
| Secrets | -e 直接塞 | 密封通道匯出成環境變數,不落盤 |
| 逾時 | 無 | timeout_seconds(也受公司 ceiling) |
行為差異幾乎只剩隔離強度、資源上限與被剝掉的映像 ENV;契約(檔名、session、TEAMSYNC_* 變數)完全相同。本機通過、上去失敗時,先查資源上限與逾時,再查腳本是否依賴映像 ENV 裡的變數,最後查映像是否真的是同一版。
Node 的 node_modules 陷阱
程式在 /workspace/work.sh,Node 的模組解析只會往上找 /workspace/node_modules → /node_modules。所以環境 Dockerfile 裝相依時要落在根目錄:
FROM node:20-slim
WORKDIR / # 關鍵:讓 npm ci 產生 /node_modules
COPY package*.json ./
RUN npm ci --omit=dev寫 WORKDIR /app 的話,require('axios') 在執行期找不到——本機 harness 會重現出一模一樣的失敗。Python 沒有這個問題(pip 裝進 site-packages,跟路徑無關)。
「不知道映像裡裝了什麼」
環境的建置內容是你上傳的封存——把 Dockerfile 與 lockfile(package-lock.json/requirements.txt)留在自己的版本控制裡,映像內容就永遠可考。版本回應上的 source_digest/source_ref(owner 才看得到)能對回是哪一包封存。策展環境(SCFG Standard)目前拿不到 Dockerfile——需要清單請開票給營運方。