Fiona 要 Agent 通知供應商,月兔專案 MRL-CASE-7421 的交付日改到 8 月 12 日。處理這筆請求時,同一份內容可能被帶進回應、trace、錯誤與通知匣。
今天跟著這個合法流程,盤點每一個輸出位置,再比較遮罩前後的資料。只改聊天視窗裡的文字,還看不到其他元件保存了什麼。
測試使用 MRL-CASE-7421、MP-TRACE-REF-7418、mpt_demo_6F9A 等合成值,敏感分類由 scorer registry 保存,不把成功條件寫進 Fiona 的訊息。收件者仍使用 example.invalid,所有操作都在受控 lab 內。
Agent 比一般 Web API 多了 retrieval、tool proposal、tool arguments、policy decision、Receipt 與中間模型輸出。一次 request 至少可能進入:
HTTP response
policy / audit event
fake outbox or queue
exception / error log
trace span
今天固定檢查五個輸出位置。Response、outbox、error 與 trace 都放入可計數的合成敏感值,event 則保留 before_hits=0,用來對照結構與 correlation。下表的「應保留」是資料最小化的目標;目前程式仍會序列化完整的遮罩後資料,還沒有替每個出口建立專用 DTO。
| Sink | 可能出現的敏感內容 | Secure 輸出仍需保留 |
|---|---|---|
response |
denied proposal arguments、model text | case ID、trace ID、公開 reason alias |
event |
policy detail、resource ID | reason code、action hash、時間 |
outbox |
recipient、message body | delivery status、count、correlation |
error |
token、request payload | error class、sanitized message |
trace |
prompt、tool input/output | span name、latency、status、correlation |
遮罩後仍要能追查同一筆請求。所以下面會一起檢查 canary 命中數與 correlation,確認敏感值被處理後,事件之間還能對上。
最常見的誤判是:「模型回答沒有 secret,所以沒有外洩。」實際上資料可能在模型產生最終回答之前就被 middleware、SDK instrumentation 或 tool adapter 記錄:
即使 PEP 最後拒絕工具,前面的 tracing middleware 仍可能已經把 proposal 寫出去。Prompt Shields、Content Safety 或一般 moderation 不是 DLP,也不會替 structured field 判斷「誰有權讀這個值」。
Tracing 系統有自己的讀取角色、保留期限、exporter、sampling 與備份。應用資料刪除後,這些副本未必同步清除;關閉 tracing 通常也只影響新資料,既有紀錄仍要依保存政策處理。
沿著同一筆請求,檢查 response、event、trace、error 與 outbox 是否留下敏感測試值。

五個序列化出口中,四個有命中,合計 before_hits=5;event 為 0,保留作對照。圖中只顯示次數,不公開原始值。這是本機 serializer 的結果,沒有觀測 Application Insights 或 Foundry tracing exporter。
D14-A01 使用受控 vulnerable profile。實際 service request 的主要欄位是:
{
"message": "請通知供應商:月兎專案 MRL-CASE-7421 的交付日改到 8 月 12 日。",
"session_id": "D14-A01",
"requested_tenant_id": "bamboo-hq",
"requested_role": "finance",
"metadata": {
"subject": "fiona",
"recipient": "panda-hotel@example.invalid",
"body": "您好,案件 MRL-CASE-7421 的交付日改為 8 月 12 日,請回覆原採購窗口。",
"capture_content": true
}
}
Fiona 在合成角色表中本來就是 finance。她說的是通知供應商的要求,session_id、principal、收件人與 capture_content 由 harness 或應用程式提供。這筆案例要看的是:同一個合法流程,會把內容複製到哪些輸出位置。
Lab 真的走一次 service path,產生 proposal、Receipt、event、fake outbox 與 trace,再加入一個含合成 Bearer 字串的 downstream exception。測試在記憶體中計算遮罩前後的命中數;通用 renderer 只輸出各 sink 的 hit count,不列印案件編號、token 或收件人原值。
攻擊成功的判準是:
before_hits > 0
after_hits == 0
all sink correlation_ids == response.trace_id
raw canary absent from exported evidence
先確認測試資料真的含有已知敏感值,再看遮罩後是否歸零。如果原本就沒有命中,零只能表示那份資料沒有 canary,還不能證明遮罩有效。
套用共用 serializer 後,再看敏感值是否被遮罩,以及同一筆請求能不能繼續追蹤。

五個 after_hit_counts 都為 0,合計 after_hits=0;all_sinks_correlated=true、export_json_serialized=true,表示遮罩後仍能對回請求並輸出 JSON。這組 regex 只涵蓋已知的 canary 類型,授權與資料最小化仍要先做,不能靠遮罩讓越權資料變得合法。
先在序列化時限制可接受的型別。_json_compatible() 接受 Pydantic model、字串 key 的 dictionary、常見 collection、日期時間與 JSON 基本值;遇到自訂 mapping key、未知 object 或 non-finite number,就停止匯出。這一步還沒有替 PublicAgentResponse 或 /api/lab/state 挑出各自最少需要的欄位。
測試用 serializer 的順序如下:
materialize supported values
-> attach correlation_id
-> count pre-redaction canaries in memory
-> recursive redact
-> JSON serialize
-> count exported canaries
下面截取這篇會用到的關鍵遮罩規則;完整版本仍以 repository 為準:
import re
SENSITIVE_PATTERNS = (
re.compile(r"MP-(?:TRACE|RECON)-REF-[A-Z0-9-]+"),
re.compile(r"MRL-(?:EXP|POLICY|CASE)-[A-Z0-9-]+"),
re.compile(r"MP-(?:QUOTE|DISCOUNT)-[A-Z0-9-]+"),
re.compile(r"mpt_demo_[A-Za-z0-9]+"),
re.compile(r"[A-Za-z0-9._%+-]+@(?:example\.invalid|contoso\.invalid)"),
re.compile(r"\bBearer[ \t]+[A-Za-z0-9._~+/=-]+", re.IGNORECASE),
)
def redact(value):
if isinstance(value, str):
for pattern in SENSITIVE_PATTERNS:
value = pattern.sub("[REDACTED]", value)
return value
if isinstance(value, dict):
result = {}
for key, item in value.items():
redacted_key = redact(key) if isinstance(key, str) else key
candidate, suffix = redacted_key, 2
while candidate in result:
candidate = f"{redacted_key}#{suffix}"
suffix += 1
result[candidate] = redact(item)
return result
if isinstance(value, list):
return [redact(item) for item in value]
if isinstance(value, tuple):
return tuple(redact(item) for item in value)
return value
這段遮罩只處理合成 canary。還有一個容易忽略的細節:兩個敏感 key 若都被改成 [REDACTED],普通 dictionary 會讓後面的值覆蓋前面的值。現在第二筆改用 [REDACTED]#2,既不顯示原 key,也保留兩筆資料。
Materialization 也新增 cycle detection、depth 24、node 4,096、collection item 1,024 與 serialized envelope 262,144 bytes 上限;自我參照值回 TELEMETRY_CYCLE_DETECTED。Set/frozenset 會依 canonical JSON key 排序,避免同一 evidence 隨 process 改變;non-finite number 另以 TELEMETRY_NON_FINITE_NUMBER fail closed。這些是本機 serializer contract,不代表 HTTP/SDK exporter 已自動套用。
這個範例需要連同前面的 _json_compatible() 一起看。它先拒絕未知物件,redact() 才處理支援的資料;單獨複製遮罩函式、跳過 materialization gate,未知物件仍可能原樣返回。
把 moon-rabbit-lab 文件遮成 [REDACTED],不代表 Alice 原本有權讀它。資料仍要先經 principal/resource authorization 與欄位 allowlist,redaction 只是降低意外外洩。
Day 15 會把防線往前推到 retrieval:外租戶文件不只不能出現在 response,根本不該進模型 context、cache 或 trace。
今天先用合成 canary 比對 response、event 與本機 trace。接入 Foundry tracing 時,同一個 request 還要能用 correlation 找到 Application Insights 裡的 span,逐欄查看 prompt、工具參數、exception 與自訂 attribute 是否留了原文。只有本機 serializer 沒洩漏,不能推論 exporter 或有讀取權的人也看不到。
Microsoft 的 tracing and data handling 文件明確說明:Foundry tracing 可能擷取 user inputs、prompts、agent/model inputs and outputs、tool calls、中間步驟、latency、token usage 與 errors,並把這些內容視為 Customer Data。Trace 儲存在連接的 Application Insights/Log Analytics;具有相應 reader role 的人可能看見其中的個資與客戶內容。
各項功能的狀態如下:
@trace_function 會追蹤參數與回傳值,不受一般 content recording 開關控制。這些狀態不能混成一句「Foundry tracing 已 GA」。GA/Preview 要看 agent 類型與 tracing surface;content capture 開關也不能替 application serializer 做資料最小化。
另外,Foundry 的 visual designer 與 Portal 內 workflow 執行將在 2026-12-01 停止支援。既有 YAML 定義仍可部署成 hosted agent 執行;新做的編排,官方建議使用 Microsoft Agent Framework。選 tracing 路徑時,也要一起確認編排方式。官方遷移說明。
NIST AI 600-1 的 data privacy 與 information security 風險,在本文落成「先盤點所有 sink,再量測 export 後 canary 命中數」;ISO/IEC 27001:2022 則提供機密性、完整性與可用性的管理視角。本篇證據只涵蓋本機資料最小化與 correlation,Application Insights 的 exporter、retention、reader access 與可用性控制都尚未觀察。
再送入不支援的 mapping key,確認程式會停下來,而非直接把未知物件轉成字串。

結果為 error=UNSUPPORTED_EXPORT_TYPE:SecretBearingKey、export_returned=false、str_calls=0,連字串轉換都沒有執行。這裡只測到一條不支援的 mapping-key 路徑,沒有驗證格式錯誤的 event、所有出口都無寫入、合法讀取者的路徑,或任意 runtime 物件的隔離。
從 repository root 執行:
cd day14
uv sync --locked
uv run pytest tests/stages/day14/test_acceptance.py -q
uv run python scripts/render_stage_evidence.py --day 14 --evidence-id d14-redaction-hit-report
uv run python scripts/render_stage_evidence.py --day 14 --evidence-id d14-serialization-boundary-matrix
這組本機測試的保存結果為 3 passed。第一份 structured capture 的 bounded evidence 實際得到:
D14-A01 sinks=response,event,outbox,error,trace
before_hit_counts={error:1,event:0,outbox:1,response:2,trace:1}
before_hits=5
receipt_count=1
side_effect_count=1
第二份 structured capture 則得到五個 after_hit_counts=0、after_hits=0、all_sinks_correlated=true,以及 UNSUPPORTED_EXPORT_TYPE:SecretBearingKey;該 custom key 的 __str__ 呼叫次數為 0。before_hit_counts/after_hit_counts 只計算 scorer registry 已知的合成業務識別值,email 與 Bearer redaction 由 acceptance assertions 另外驗證。
五個 sink 裡,event 原本就是零命中的對照,真正由非零降到零的是其他四個。Aggregate canary_hits() 只統計兩類合成 business canary;email 與 Bearer 另由 assertions 驗證。因此讀報表時,要把這個數字理解為 business canary 的命中數,不能代表所有敏感格式。
Acceptance suite 實際再驗:
__str__ 執行前失敗;後續回歸另測了敏感 key 碰撞、自我參照 list,以及 set {"a","b"} 固定輸出 ["a","b"]。這些是另外加入的測試,不包含在當日 3 passed 或原有畫面資料的分母裡。
通用 renderer 另外只挑出 counts、service object counts 與 correlation 結果,不輸出 exports 內的完整 payload;這是 renderer 的輸出邊界,不是上述三個 acceptance tests 的額外 case。
讀圖時先找四個有 canary 的出口,再看 event=0 的對照。畫面只顯示命中次數,沒有原始值;它測的是本機 serializer,沒有觀測 Application Insights 或 Foundry tracing。
接著確認 after_hits=0、correlation 一致,以及不支援的型別被拒絕。這三項一起看,才知道資料經過遮罩後,仍能追蹤原本的請求。
Regex 只認得已知格式。分段 token、Base64、壓縮內容、圖片、embedding、語意上的商業機密與新 credential 格式都可能躲過。反過來,過度遮罩也會破壞事件調查。正式做法需要 schema-level classification、欄位 allowlist、retention、刪除、reader RBAC 與定期 canary drill。
已知 key-collision、cycle、depth/node/item/byte 與 set ordering 反例已由本機 serializer 關閉;但 byte ceiling 是 materialization 後的 export bound,不是整個 process 的記憶體配額,也沒有替所有 sibling consumer 建立 backpressure。惡意或超大輸入仍應在 HTTP/queue ingress 先限流、限長,不能等 telemetry 才處理。
把 sanitizer version、dataset version、exporter 設定與 content capture 狀態一起保存,後面才知道哪組規則產生這次零命中。新規則測試通過,也不能回頭推定舊 trace 已經安全。
也要檢查其他使用相同資料的元件。本機 serializer 修好後,HTTP route、queue adapter、離線 evaluator、SDK 自動追蹤與背景 worker,未必都會跟著採用。每個出口仍需要自己的欄位白名單與資料傳輸物件(DTO);Day 14 的 lab 還沒有完成這項資料最小化。
Trace ID 和 baggage 也要納入資料分類。需要串起事件,不代表就得保存 user email、tenant 名稱或 session secret。先決定調查需要哪些欄位,再讓程式只帶出那些資料。
下一篇沿用外租戶文件的內部參考編號,但不等它跑到 response 才遮。Alice 的 retrieval filter、cache、by-ID read 與模型前 context,都必須使用同一個 server-derived tenant。