AI Coding Agent 出站流量稽核不是看一眼「隱私」開關,就猜程式碼有沒有離開電腦。真正可核對的做法,是準備一個沒有機密的一次性 Git repo,把「程式讀了哪些檔案、哪個程序在主機網路堆疊送出什麼、外部觀測點是否看到流量跨界、伺服器是否回覆接受」串成同一張收據。本文用 2026 年 9 月的 ZCode 事件當案例,帶你做出本機 macOS coding agent 能重跑的 7 步安全稽核;Windows/Linux 也能保留相同收據欄位,但要換成各自的程序與封包工具。
你不需要先會封包逆向。完成後,你至少能回答四件事:代理程式是否碰到界線外檔案、是否在本機打包、是否建立出站連線,以及目前證據最多能支持到哪一層。最重要的判讀公式是:本機打包 ≠ 嘗試上傳 ≠ 伺服器接受。
先看結論:一張稽核收據要有四種證據
- 檔案:測試 repo、local-only canary、前後檔案清單與雜湊。
- 程序:app 版本、二進位雜湊、PID/PPID、實際讀檔紀錄。
- 網路:程序歸屬、目的地、方向、時間與加密封包長度。
- 接受:具明確成功語意、綁定該次 upload 的應用層回應,再用 request ID/服務端稽核紀錄交叉核對。

只看到新 tar、zip 或暫存檔,最多只能標成「疑似 staging」;還要用目標程序、時間與 manifest 交叉核對,才能說本機封裝發生。防火牆拒絕紀錄或 TCP SYN,代表「嘗試連線」。主機上的 PKTAP 看到 TLS/QUIC frame,只能證明指定 capture 介面/網路堆疊觀察到出站加密 frame,通常看不到裡面是哪個檔案;要證明它真的跨出主機邊界,還要和閘道器、外部 capture 或對端紀錄配對。即使遠端 TCP ACK 回來,也只是傳輸層收到了位元組;要說「應用程式接受」,仍要有綁定該次 upload、具明確成功語意的 2xx 回應與 response body,再用 request ID 或 provider audit log 交叉核對。request ID 單獨存在不是成功證明。
ZCode 事件教我們什麼?先把版本和事實鎖死
原始鑑識文章在 2026 年 9 月 18 日發布,隔日補上重要更正。研究者在 ZCode 3.12.3 的一個商業專案看到:工作區約 345,549,173 bytes、本機加密封裝 313,070,842 bytes、失敗計數 564。這個大封裝留在 pending,路由器紀錄也沒有看到它離開區網,因此不能寫成「313 MB 專案已外洩」。
同一篇更新另記錄一個小型公開 repo:538 個檔案、壓縮加密後約 15 KB,研究者表示客戶端狀態標成 accepted;公開文章沒有附可獨立配對的原始 response/request ID,所以本文把它保留為作者的版本限定判讀,不升格成獨立證實的服務端收據。這是另一組樣本,而且仍不證明伺服器保留多久、是否有人存取,或是否被用於模型訓練。大型失敗樣本與小型 accepted-status 樣本不能混成一句話。
ZCode 官方更新紀錄顯示 3.12.3 於 9 月 17 日發布,3.14.0 於 9 月 19 日發布;3.14.0 的說明只承認修正 repository wiki 的「異常上傳」問題。原始鑑識作者表示檢查 3.14.0 後,舊版上傳管線已移除;這是作者的版本限定觀察,不是官方 changelog 的原文,公開文章也沒有附上可讓本文獨立重算的 3.14.0 artifact 雜湊與摘錄。供應商另稱雲端產生 Wiki 後會立即銷毀資料;本文沒有取得可獨立核對的服務端紀錄,因此不把這項聲明當成已證實事實。
以下數字來自上述原始鑑識,不是 AlphaLab 重新安裝舊版後得到的「親測」。這反而示範了第一條規則:每個結論都要綁定版本、條件與證據來源。不要為了重跑而從不明來源下載舊版,也不要讓舊版碰到真實專案。
為什麼設定頁不能回答「資料有沒有離機」?
「改善模型」、「索引 repo」、「雲端 Wiki」可能是不同開關、不同程序與不同後端路徑。某個選項顯示關閉,只能證明 UI 或設定值的狀態;它不會自動證明另一條資料管線沒有執行。反過來說,看到連線也不代表整個 repo 都在裡面:登入、更新檢查、模型請求與遙測都會產生網路流量。
因此,測試要把目標處理變因縮成一個,並把「允許做什麼」和「實際做了什麼」分開。為避免遠端快取污染,每輪仍會換一個等長 UUID canary;它是必須記錄、用多組配對抵銷的受控干擾,不能假裝不存在。若你還沒建立最小權限觀念,可先看 AI Agent 機密管理;若要把整個環境隔開,則搭配 Coding Agent 沙盒與 microVM。
AI Coding Agent 出站流量稽核:7 步建立安全實驗
第 1 步:先寫邊界,再建立一次性 repo
最穩妥的環境是可還原快照的 macOS 測試 VM;次選是沒有 iCloud、SSH agent、雲端憑證與其他工作檔案的專用測試帳號。關閉共享資料夾、共享剪貼簿與主機 Home 掛載,只給測試帳號最低權限。若產品只有 macOS 桌面版,用原生 macOS VM 比把另一個 Linux CLI 塞進 Docker 更接近真實行為。
先建立一個內容永久公開也無妨的 repo。下例會產生公開 committed marker,並先保留一個被 .gitignore 排除的 local-only 路徑;真正的 local-only UUID 要等第 2 步、在每台 clone 的每一輪各自新建,不能跨條件重用。local-only UUID 不會進入公開 Git、檔名或 prompt。公開 marker 可能早已被模型或搜尋引擎看過,不能證明這台電腦傳了它。
set -e
umask 077
LAB_ROOT="$(mktemp -d "$HOME/.agent-egress-audit.XXXXXX")"
[[ -n "$LAB_ROOT" && -d "$LAB_ROOT" ]] || {
echo 'FAIL: could not create a private lab directory'
exit 1
}
case "$LAB_ROOT" in
"$HOME"/.agent-egress-audit.*) ;;
*) echo 'FAIL: unexpected lab path'; exit 1 ;;
esac
REPO="$LAB_ROOT/repo"
EVIDENCE_ROOT="$LAB_ROOT/evidence"
SETUP_RUN="$EVIDENCE_ROOT/00-setup"
AUDIT_ID="$(uuidgen)"
PUBLIC_ID="$(uuidgen)"
mkdir -p "$REPO/src" "$REPO/fixtures" "$SETUP_RUN" "$LAB_ROOT/no-hooks"
git init --template="$LAB_ROOT/no-hooks" -b main "$REPO"
git -C "$REPO" config user.name "Audit Fixture"
git -C "$REPO" config user.email "audit-fixture@example.invalid"
git -C "$REPO" config core.hooksPath "$LAB_ROOT/no-hooks"
git -C "$REPO" config commit.gpgSign false
printf '%s\n' '# Public egress audit fixture' > "$REPO/README.md"
printf '%s\n' 'export const add = (a, b) => a + b;' > "$REPO/src/add.js"
printf '%s\n' "FAKE_API_KEY=NOT_A_SECRET_PUBLIC_${PUBLIC_ID}" \
> "$REPO/fixtures/fake.env.example"
printf '%s\n' 'local-only-fake-secret.txt' > "$REPO/.gitignore"
git -C "$REPO" add README.md .gitignore src fixtures
git -C "$REPO" commit --no-gpg-sign -m "audit fixture ${AUDIT_ID}"
git -C "$REPO" rev-parse HEAD > "$SETUP_RUN/git-head.txt"
接著建立一份只含測試路徑與 audit ID 的交接檔;它不保存 token,也不保存 local-only canary 值。把它複製到這台拋棄式 VM 的固定 bootstrap 路徑,讓重開機與 clone 後仍能載入;它不設定單輪 $RUN,避免四組條件共用檔名而互相覆寫。
LAB_ENV="$LAB_ROOT/lab.env"
printf 'umask 077\nexport LAB_ROOT=%q\nexport REPO=%q\nexport EVIDENCE_ROOT=%q\nexport AUDIT_ID=%q\n' \
"$LAB_ROOT" "$REPO" "$EVIDENCE_ROOT" "$AUDIT_ID" > "$LAB_ENV"
chmod 600 "$LAB_ENV"
BOOTSTRAP="$HOME/.agent-egress-audit-lab.env"
[[ ! -e "$BOOTSTRAP" ]] || {
echo 'FAIL: bootstrap path already exists; inspect it before continuing'
exit 1
}
cp "$LAB_ENV" "$BOOTSTRAP"
chmod 600 "$BOOTSTRAP"
printf '固定環境:source %q\n' "$BOOTSTRAP"
再用下面的 fail-closed 檢查,確定 repo 有有效 HEAD,而且 local-only marker 從未出現在任何 Git ref 的歷史:
git -C "$REPO" rev-parse --verify HEAD >/dev/null || {
echo 'FAIL: repository has no valid HEAD'
exit 1
}
HISTORY_MATCHES="$(git -C "$REPO" log --all --format='%H' \
-- local-only-fake-secret.txt)" || {
echo 'FAIL: could not inspect Git history'
exit 1
}
if [[ -n "$HISTORY_MATCHES" ]]; then
echo 'FAIL: local-only marker exists in Git history'
exit 1
else
echo 'OK: local-only marker is absent from Git history'
fi
set +e
不要放真的、已撤銷的或過期的 API key;它們仍可能觸發掃描器或帶來風險。測試值要明寫 NOT_A_SECRET 並加隨機 UUID。若你要推到公開 Git host,使用拋棄式帳號,先完成 push 再開始封包擷取,避免把 Git 自己的流量算進 agent。
完成第 1 步、安裝待測 app,但尚未登入或執行任務時,關機並封存一個 sealed baseline。實驗根目錄放在測試帳號的私有持久目錄,不依賴重開機時可能清除的 TMPDIR。第 6 步的四個條件都從 baseline 建立獨立 VM clone;不要在同一台 VM 上回滾後繼續寫,否則該輪證據會跟著消失。
第 2 步:固定版本與可比較條件
每輪先選一個不重複的條件名稱,建立獨立 $RUN。把下面的 CONDITION 依序換成 01-logged-out-idle、02-logged-in-feature-off、03-logged-in-feature-on 或 04-version-comparison;每輪都建立新目錄,絕不清空或重用。指令會印出該輪所有新 Terminal 要貼上的兩個 source。
source "$HOME/.agent-egress-audit-lab.env" || exit 1
[[ -n "$LAB_ROOT" && -d "$LAB_ROOT" \
&& "$REPO" == "$LAB_ROOT/repo" && -d "$REPO" \
&& "$EVIDENCE_ROOT" == "$LAB_ROOT/evidence" \
&& -d "$EVIDENCE_ROOT" ]] || {
echo 'FAIL: bootstrap paths are missing or inconsistent'
exit 1
}
CONDITION='01-logged-out-idle' # 每一輪換成新的條件名稱
[[ "$CONDITION" =~ ^[0-9]{2}-[a-z0-9-]+$ ]] || {
echo 'FAIL: unsafe condition name'
exit 1
}
RUN="$EVIDENCE_ROOT/$CONDITION"
mkdir "$RUN" || {
echo 'FAIL: evidence directory already exists; choose a fresh condition'
exit 1
}
RUN_ENV="$RUN/run.env"
printf 'export RUN=%q\nexport CONDITION=%q\n' \
"$RUN" "$CONDITION" > "$RUN_ENV"
chmod 600 "$RUN_ENV"
# 每一輪才建立全新的 local-only canary;不得跨條件重用
CANARY_ID="$(uuidgen)" || exit 1
[[ -n "$CANARY_ID" ]] || exit 1
printf '%s\n' '# INERT TEST DATA; NOT A CREDENTIAL' \
"LOCAL_ONLY_CANARY=LOCAL_ONLY_NOT_A_SECRET_${CANARY_ID}" \
> "$REPO/local-only-fake-secret.txt"
git -C "$REPO" check-ignore -v local-only-fake-secret.txt \
> "$RUN/canary-ignore.txt" || exit 1
git -C "$REPO" status --short --ignored -- local-only-fake-secret.txt \
> "$RUN/canary-status.txt" || exit 1
grep -qx '!! local-only-fake-secret.txt' "$RUN/canary-status.txt" || {
echo 'FAIL: canary is not uniquely ignored'
exit 1
}
shasum -a 256 "$REPO/local-only-fake-secret.txt" \
> "$RUN/canary.sha256" || exit 1
unset CANARY_ID
printf '新 Terminal 先執行:source %q && source %q\n' \
"$HOME/.agent-egress-audit-lab.env" "$RUN_ENV"
接著記錄 UTC 起訖時間、作業系統、app 版本、下載來源、主執行檔與 Electron app.asar(若存在)的個別雜湊、登入狀態、模型、索引/Wiki/遙測設定、VPN/proxy、防火牆規則與 Git commit。有原始安裝檔時另記 installer hash;不要把 launcher hash 誤叫成整個 app 的 artifact hash。只記允許清單,不要把完整環境變數、Keychain、.npmrc 或 auth log 倒進報告。
(
set -e
set -o pipefail
APP='/Applications/ZCode.app' # 換成實際 app 路徑
INFO="$APP/Contents/Info.plist"
EXECUTABLE_NAME="$(plutil -extract CFBundleExecutable raw -o - "$INFO")"
EXECUTABLE="$APP/Contents/MacOS/$EXECUTABLE_NAME"
ASAR="$APP/Contents/Resources/app.asar"
date -u '+%Y-%m-%dT%H:%M:%SZ'
sw_vers
uname -mrs
git --version
git -C "$REPO" rev-parse HEAD
git -C "$REPO" status --short --ignored
printf 'bundle_short_version='
plutil -extract CFBundleShortVersionString raw -o - "$INFO"
printf 'bundle_build_version='
plutil -extract CFBundleVersion raw -o - "$INFO"
printf 'main_executable_sha256_record='
shasum -a 256 "$EXECUTABLE"
if [[ -f "$ASAR" ]]; then
printf 'app_asar_sha256_record='
shasum -a 256 "$ASAR"
else
printf 'app_asar_sha256=not_present\n'
fi
codesign --verify --deep --strict --verbose=2 "$APP" 2>&1
codesign -dv --verbose=4 "$APP" 2>&1
) > "$RUN/environment.txt" || {
echo 'FAIL: environment fingerprint is incomplete' >&2
exit 1
}
不要把「目前官網最新版」當成永久事實;把查核日期與實際 artifact hash 寫進收據。像 Mistral 隱私設定稽核一樣,設定截圖只是其中一格,不是全部答案。
第 3 步:留下本機讀檔與封裝痕跡
執行前後各做一次檔案 metadata 快照,並用 macOS 內建 fs_usage 在限定時間內記錄 open、read、exec 與 pathname。若另開 Terminal,先執行第 2 步印出的兩個 source。不要直接在日常帳號掃整個 Home;檔名本身也可能是敏感資料。
(set -o pipefail; \
find "$REPO" -xdev -type f -exec stat -f '%Fm|%z|%N' {} + | \
LC_ALL=C sort) > "$RUN/files-before.txt" || exit 1
(set -o pipefail; \
find "$REPO" -xdev -type f -exec shasum -a 256 {} + | \
LC_ALL=C sort) > "$RUN/content-before.sha256" || exit 1
touch "$RUN/run-start.marker"
# 新 Terminal 先載入 lab.env 與本輪 run.env;最多執行 5 分鐘
if ! sudo /usr/bin/env TZ=UTC0 /usr/bin/fs_usage \
-w -f filesys -f pathname -f exec -t 300 \
> "$RUN/fs-usage.log" 2>&1; then
echo 'INCOMPLETE: fs_usage failed'
exit 1
fi
if grep -qi 'buffer overrun' "$RUN/fs-usage.log"; then
echo 'INCOMPLETE: fs_usage reported a buffer overrun'
exit 1
fi
# 測試結束後
(set -o pipefail; \
find "$REPO" -xdev -type f -exec stat -f '%Fm|%z|%N' {} + | \
LC_ALL=C sort) > "$RUN/files-after.txt" || exit 1
(set -o pipefail; \
find "$REPO" -xdev -type f -exec shasum -a 256 {} + | \
LC_ALL=C sort) > "$RUN/content-after.sha256" || exit 1
METADATA_DIFF_RC=0
diff -u "$RUN/files-before.txt" "$RUN/files-after.txt" \
> "$RUN/files-diff.txt" || METADATA_DIFF_RC=$?
[[ $METADATA_DIFF_RC -le 1 ]] || { echo 'FAIL: metadata diff failed'; exit 1; }
CONTENT_DIFF_RC=0
diff -u "$RUN/content-before.sha256" "$RUN/content-after.sha256" \
> "$RUN/content-diff.txt" || CONTENT_DIFF_RC=$?
[[ $CONTENT_DIFF_RC -le 1 ]] || { echo 'FAIL: content diff failed'; exit 1; }
find "$REPO" -xdev -type f -newer "$RUN/run-start.marker" \
-print > "$RUN/new-files.txt" || exit 1
date -u '+%Y-%m-%dT%H:%M:%SZ' > "$RUN/run-ended-at-utc.txt"
fs_usage 的 command name/thread ID 與時間能支持「該路徑在相近時間被存取」,但它不是完整 PID 稽核:預設會排除部分 shell,cache、memory map 與極短事件也可能漏掉。新壓縮檔只是疑似 staging,需再用 process tree、時間與 manifest 核對。即使確認本機封裝,仍不能支持「已傳出」。反過來,沒看到檔案也不等於沒打包:資料可能在記憶體完成。
第 4 步:記錄 process tree,不要只看 app 名稱
Electron app、XPC service、瀏覽器網路程序與 nsurlsessiond 都可能代送流量。新 Terminal 同樣先載入固定環境與本輪 run.env,再每秒記一次 PID、PPID 與可執行檔名稱;刻意不輸出完整 arguments,避免 token 出現在 log。下例最多 5 分鐘,也可提前按 Control-C:
for _sample in {1..300}; do
printf '# %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
/bin/ps -axo pid=,ppid=,uid=,etime=,comm=
sleep 1
done > "$RUN/processes.log"
把 app 主程序、子程序與有效程序名稱排成時間線,再和封包時間對齊。這個想法也適用於 Claude Code、Codex 與 MCP 權限稽核:工具名稱不是安全邊界,實際程序與可存取資源才是。
第 5 步:在 macOS 用 PKTAP 擷取程序歸屬
在只有假資料的測試 VM 內,Apple 版 tcpdump可用 pktap,all 擷取 loopback、實體與 tunnel 介面,PcapNG 還能保留程序、PID、介面與方向 metadata。新 Terminal 先載入固定環境與本輪 run.env。先保留封包前 256 bytes,降低意外收進敏感內容的風險;完整 snap length 只適合完全拋棄式環境。
sudo -v
if ! sudo /usr/sbin/tcpdump \
-Z "$(id -un)" -p -i pktap,all -s 256 -B 4096 -U \
-G 300 -W 1 \
-w "$RUN/traffic.pcapng" 2> "$RUN/tcpdump.log"; then
echo 'INCOMPLETE: tcpdump capture failed'
exit 1
fi
grep -Eq '^[0-9]+ packets captured$' "$RUN/tcpdump.log" && \
grep -Eq '^0 packets dropped by kernel$' "$RUN/tcpdump.log" || {
echo 'INCOMPLETE: tcpdump final statistics are missing or report drops'
exit 1
}
# 約 5 分鐘;若 300 秒後沒有新封包觸發輪替,按 Control-C
# 也可提早 Control-C;之後以 UTC 解碼
if ! TZ=UTC0 /usr/sbin/tcpdump -nn -tttt -k INPSD \
-r "$RUN/traffic.pcapng" \
-Q 'dir=out' > "$RUN/outbound-readable.txt"; then
echo 'INCOMPLETE: saved trace could not be decoded'
exit 1
fi
檢查 tcpdump.log;只要有 kernel drops,這份 trace 就是不完整。等你從 process log 整理出主程序和 helpers 的 PID,再對已保存的 trace執行:TZ=UTC0 /usr/sbin/tcpdump -nn -tttt -k INPSD -r "$RUN/traffic.pcapng" -Q '(pid=1234 or epid=1234) and dir=out' > "$RUN/agent-outbound.txt"。把 -k 顯示的介面一起保留:pktap,all 可能同時看到 loopback、VPN 內外層與實體介面,不能把它們相加後當成「離機 bytes」。DNS 可能由 mDNSResponder 代送,也可能走 DoH;用時間、回傳 IP 與後續連線交叉比對,不要因為找不到明文 DNS 就判定沒有連線。
若你習慣 GUI,LuLu 或 Little Snitch 能協助看 connection attempt 與 responsible app,但仍應搭配 packet capture。macOS 內建 Application Firewall 主要管入站,不是本題需要的出站稽核器。
第 6 步:分清背景基線、多組配對與版本參照

每一輪都從同一個 sealed baseline 建立獨立 VM clone,讓 app 以全新程序啟動,擷取任務前後各 30~60 秒。每台 clone 先回到第 2 步建立全新的條件目錄;若 mkdir 因已存在而失敗就停下來,不能覆寫。下面四組用途不同,不能全部當成單變因 A/B test:
- A/背景脈絡:未登入、閒置,量出更新檢查與背景連線;它和有 prompt 的組別不構成單變因比較。
- B/控制組:已登入、索引/Wiki 關閉,做相同的唯讀程式說明任務;使用該輪新 UUID。
- C/處理組:維持 B 的登入、檔案結構、prompt 與環境,把指定功能開啟;另用一個等長新 UUID,避免沿用 B 可能已被遠端處理的值。
- D/版本參照:目前修正版對照已有的歷史證據;只有手上已有合法舊 artifact、隔離 VM 與拋棄式帳號,且能固定其他條件時才做版本 pair,否則只標「來源參照」或 N/A。
每輪結束先停止 app 與 collectors,確認 fs_usage 沒有 buffer overrun、tcpdump 沒有 kernel drops,再為 $RUN 內所有原始檔建立 SHA-256 manifest。接著關機,保留整台 clone,或在關機後透過 hypervisor 的磁碟匯出/唯讀掛載,把該輪目錄複製到獨立證據庫並重算 manifest。測試執行期間不要開共享資料夾;也不要在證據尚未外移驗證前 revert 或刪除 clone。
因此,能回答「這個功能開關可能造成什麼差異」的是 B ↔ C 的多組配對;A 只幫你辨認背景噪音,D 若沒有同條件舊版實驗,就只能提供歷史脈絡。B/C 的 UUID 值不同,所以它不是字面上的單變因實驗:保持檔名與 byte 長度一致、至少重跑三組新 pair、交錯 B/C 順序,只解讀跨 pair 都一致的程序/目的地/接受狀態差異,不用細小 byte 差做因果結論。
routine prompt 可以是:「不要修改檔案,說明 src/add.js。」正向 canary prompt 則只給檔名,不把值寫進 prompt:「讀取 local-only-fake-secret.txt,回報等號後的完整值。」若經確認的遠端模型精確回傳這個不在 prompt、不在公開 Git 的 UUID,這是遠端處理該 marker 的強證據;它仍不回答保存期限或訓練用途。
如果要比較 byte count,至少重跑成對條件,且只把它當作 socket/frame 的比較值。TLS header、重傳、壓縮、HTTP/2 multiplexing、VPN 內外層與 padding 都會讓「封包 bytes = repo 大小」這個推論失效。
第 7 步:填寫 audit receipt,再做 pass/fail 決策
停止所有 collectors 後,先確認 agent 沒有改寫 canary、取消 ignore 或把它 force-add 進 Git,再為該輪原始檔建立 manifest,最後把整個目錄設成唯讀;後續只分析複本。任何一步失敗都不要進入判讀:
git -C "$REPO" check-ignore -v local-only-fake-secret.txt \
> "$RUN/canary-ignore-after.txt" || exit 1
if git -C "$REPO" ls-files --error-unmatch \
local-only-fake-secret.txt >/dev/null 2>&1; then
echo 'FAIL: canary became tracked'
exit 1
fi
POST_HISTORY="$(git -C "$REPO" log --all --format='%H' \
-- local-only-fake-secret.txt)" || exit 1
[[ -z "$POST_HISTORY" ]] || { echo 'FAIL: canary entered Git history'; exit 1; }
shasum -a 256 -c "$RUN/canary.sha256" \
> "$RUN/canary-verify.txt" || {
echo 'FAIL: canary content changed during the run'
exit 1
}
(
set -e
set -o pipefail
cd "$RUN"
find . -type f ! -name RUN-MANIFEST.sha256 \
-exec shasum -a 256 {} + | LC_ALL=C sort
) > "$RUN/RUN-MANIFEST.sha256" || {
echo 'FAIL: could not hash the complete evidence set'
exit 1
}
chmod -R a-w "$RUN" || {
echo 'FAIL: could not make the evidence set read-only'
exit 1
}
下面是尚未執行的空白格式範本;其中 unknown 不是測試結果。每個 observation 都要附 artifact 與時間,不要先寫結論再找圖佐證:
audit_id: "UUID"
started_at_utc: "YYYY-MM-DDTHH:MM:SSZ"
ended_at_utc: "YYYY-MM-DDTHH:MM:SSZ"
agent:
name: "AGENT_NAME"
version: "VERSION"
main_executable_sha256: "SHA256"
app_asar_sha256: "SHA256_OR_NOT_PRESENT"
installer_sha256: "SHA256_OR_NOT_AVAILABLE"
fixture:
git_commit: "GIT_COMMIT"
local_canary_sha256: "SHA256"
condition:
login: test-account
indexing_or_wiki: off
prompt_id: routine-readonly-v1
observations:
file_access: unknown # observed / not_observed / incomplete
local_packaging: unknown
host_stack_outbound: unknown
external_boundary_observed: unknown
application_acceptance: not_established
artifacts:
- processes.log
- fs-usage.log
- traffic.pcapng
- tcpdump.log
limitations:
- tls_payload_encrypted
- plain_dns_not_observed
decision: investigate # pass / fail / investigate
Pass 不是「世界上沒有任何傳輸」,而是這個明確版本、條件和擷取窗口內,沒有超出你預先允許的資料邊界。Fail 是看到界線外讀檔、未授權封裝,或不符合政策的目的地/接受回執。只要 trace 掉包、程序歸屬不明、測試條件飄移,就填 investigate,不要把「沒看到」改寫成「不存在」。
如何判讀:你看到的證據最多能說到哪裡?
fs_usage出現 canary:程序存取了本機路徑;未證明進入請求。- 新 archive/database/temp:疑似 staging;與目標程序、時間及 manifest 配對後,才支持本機封裝,仍未證明傳輸。
- 防火牆 deny 或 SYN:嘗試連線;未證明 payload 已離機。
- 主機 PKTAP 的出站 TLS/QUIC frame:主機網路堆疊送出加密 frame;未單獨證明它跨出主機邊界,也未證明裡面是哪個檔案。
- 遠端 TCP ACK:對方傳輸層確認位元組;未證明應用程式解析或接受。
- 綁定該次 upload、具明確成功語意的 2xx response/body,再以 request ID 與 provider log 交叉核對:可支持應用層接受;request ID 單獨存在不夠,也未證明保存、人工存取或訓練。
若工具只提供模糊設定,先用 Agent Runtime Controls建立預設拒絕、最小路徑與目的地 allowlist。需要把這套驗收放進開發流程時,再延伸到 AI Agent Harness,讓每個版本自動跑相同的安全 case。
測試完成後:卸載、撤銷與憑證輪替
- 先保全
state.json、manifest、pending artifact、PcapNG 與 log 的原始副本,分別計算 SHA-256。 - 撤銷拋棄式帳號的 OAuth session、API token 與裝置登入;不要只登出 GUI。
- 移除測試防火牆規則、proxy profile 與任何測試 CA;恢復前先確認它們的精確名稱與範圍。
- 檢查 app 的 Login Items、LaunchAgents/LaunchDaemons 與 helper,再依供應商卸載方式移除。
- 若保留測試帳號,確認證據已外移後,移除精確的 bootstrap 檔
$HOME/.agent-egress-audit-lab.env;不要對 Home 目錄做遞迴刪除。 - 封存需要的收據後,刪除整個拋棄式 VM/測試帳號與公開 repo。公開內容可能已有 clone 或 cache,刪除不等於收回。
- 若誤碰真實 repo,輪替目前與 Git 歷史曾出現的憑證,並檢查服務端 access log;不要因 key 已從最新 commit 刪除就略過。
常見錯誤:最容易把證據說過頭的 6 個地方
- 把設定值當行為:開關 off 只是一個測試條件,必須再看程序與網路。
- 把公開 marker 當來源證明:它可能早就在網路上;一定要配 local-only UUID。
- 把 pending 當 accepted:失敗計數與 pending archive 恰好可能是未成功的證據。
- 把封包大小當檔案大小:協定、重傳、壓縮、padding 與其他請求都會干擾。
- 只跑一次:背景更新、連線重用與每輪新 canary 都可能製造差異;用獨立 clones 跑多組 B/C pair,交錯順序後只看一致結果。
- 為了查舊洞而降低安全:沒有合法舊 artifact 就填 N/A,用已發表鑑識當歷史基線。
FAQ:AI Coding Agent 出站流量稽核常見問題
1. 關掉「改善模型」就代表程式碼不會上傳嗎?
不代表。模型改善、repo 索引、雲端 Wiki 與一般模型請求可能是分開的資料管線;把開關狀態列入條件,再用檔案、程序與網路證據查實際行為。
2. 看到 300 MB 的加密檔,就能說 300 MB 已上傳嗎?
不能。它只證明本機封裝;依原始鑑識作者的修正,ZCode 的 313,070,842-byte 樣本停在 pending、反覆失敗,他的路由器紀錄沒有看到它離開區網。
3. tcpdump 能看到上傳了哪些原始碼嗎?
一般情況看不到。TLS 1.3 與 QUIC 會加密應用內容;PcapNG 主要讓你核對程序、目的地、方向、時間與 ciphertext 長度。不要在新手稽核中安裝攔截 CA,因為它會改變環境並產生新的敏感金鑰。
4. 沒抓到封包,是否能證明 agent 從不傳資料?
不能。你只能說「在這個版本、條件、時間窗與完整度下沒有觀察到」。連線重用、快取 DNS、DoH、VPN、trace 掉包或功能 gating 都可能影響結果。
5. 為什麼 canary 一定要是假的?
因為測試不該創造新的事故。用明寫 NOT_A_SECRET 的 UUID 就能辨認資料路徑,不需要拿真實、過期或已撤銷金鑰冒險。
6. Docker 可以取代 macOS VM 嗎?
只有 agent 原生支援 Linux 時才適合。若目標是 macOS GUI app,容器裡跑另一個版本不是同一個 artifact;macOS VM 才能同時保留產品行為與隔離性。
7. 舊版與修正版一定要親自各跑一次嗎?
不用。只有手上已有合法 artifact、拋棄式 VM 與測試帳號才跑舊版;否則把可信的歷史鑑識標為「來源證據」,自己的修正版測試標為「當前觀察」,舊版欄填 N/A。
8. 什麼證據才足以判定伺服器接受?
至少要有綁定該次 upload、具明確成功語意的應用層回應。例如 documented 2xx response/body,再以 request ID 和服務端 audit log 配對;request ID 單獨存在不夠,client accepted state 也必須能和該次 server response 因果配對。即使如此,也只能支持「該層接受」,不能自動推論保存期限、人工存取或訓練用途。
重點整理:把推測變成可重跑的驗收
- 只用永久公開也安全的 repo,加上一個 ignored、local-only、假的 UUID canary。
- 把 app 版本、artifact hash、Git commit、設定與時間窗固定下來。
- 用
fs_usage、process tree、PKTAP PcapNG 與回執串起證據鏈。 - 永遠分開「本機封裝、主機堆疊送出、外部邊界觀察、應用層接受」。
- 界定目標處理與受控干擾,用獨立 clones 做交錯順序的多組配對;不完整就判 investigate。
- 依原始鑑識作者的修正,ZCode 313 MB 樣本未成功上傳;小型公開 repo 的 accepted status 是另一組、版本限定且未附原始 response 的作者判讀。
接著閱讀
左右滑動查看更多推薦
結論:先定義你要證明的那一層
最差的稽核問題是「這個 agent 安不安全?」;最好的問題是「在版本 X、條件 Y、時間窗 Z,它是否讀取 local-only canary、是否由已識別程序送出加密位元組、是否取得應用層接受回執?」後者能被下一個人重跑,也能在升版後做回歸。
現在就先做最小的一步:建立一次性 repo 與 local-only canary,填完環境 manifest,先跑「未登入、閒置」基線。想繼續把 AI 工具用得更扎實,可瀏覽 AlphaLab AI 專區,或從 AlphaLab 課程建立完整實作路線。






