摘要
Artifact 的「heal」自動修復流程(用來處理指標損壞的 pointer,見 POST /{agent_id}/artifacts/{artifact_id}/heal)在「workspace 裡剛好只掃到一個同副檔名候選檔案」時,會不經任何確認、靜默地把損壞的 artifact 重新註冊到那個候選檔案上——即使那個檔案已經是另一個 artifact 的 entry file。結果是兩個完全不同的 artifact 記錄(不同 artifact_id、不同標題、不同建立時間)指向同一份實體檔案,其中一個的標題/身份跟它實際顯示的內容完全對不上。
重現步驟(實測紀錄)
環境:https://dev-agent.narra.nexus,xyzdev01,Nova(agent_4bf9d757910b)
- 請 Nova 建立一個新的 HTML artifact:
art_bb19e14e,標題「QA-ARTIFACT02-heal-test」,entry file 為 qa_artifact02/index.html
- 用 Bash 只刪除這一個實體檔案(
rm qa_artifact02/index.html),DB 裡的 artifact 記錄不動——模擬「pointer 損壞」情境
- 存取這個 artifact 的 raw 端點(
GET /api/public/artifacts/raw/{token}/)→ 正確回 410 {"detail":"artifact file missing on disk"},符合設計預期
- 重新整理 artifact 面板(觸發前端自動呼叫 heal)→ 再次存取同一個 artifact,變成 HTTP 200,但回傳內容是 Nova 原本就有的「Welcome to NarraNexus」歡迎頁(
last-modified: 2026-06-18),完全不是這次測試建立的內容
- 請 Nova 查詢目前的 artifact registry,確認:
art_bb19e14e [text/html] "QA-ARTIFACT02-heal-test" → welcome/index.html
art_5196fa1b [text/html] "Welcome to NarraNexus" → welcome/index.html
兩個不同的 artifact_id、不同標題,現在指向同一個實體檔案。qa_artifact02/ 目錄確認為空(total 0),workspace 裡確認只剩這一個 .html 檔案。
程式碼層根因
xyz_agent_context/artifact/_artifact_impl/heal.py 的 heal_artifact():
candidates = _scan_workspace_for_kind(workspace_root, art.kind)
if len(candidates) == 1:
only = candidates[0]
result = await registration.register_artifact(
repo=repo, agent_id=agent_id, user_id=user_id, session_id=None,
kind=art.kind, entry_path=only.workspace_path,
title=art.title, description=art.description,
target_artifact_id=artifact_id,
)
...
return HealResult(recovered=True, artifact=healed, message=f"auto-recovered from {only.workspace_path}")
_scan_workspace_for_kind()單純用副檔名掃 workspace 底下所有檔案,完全沒有排除「已經被其他 artifact_id 佔用當 entry file」的檔案。當掃描結果剛好只有 1 個候選(很容易發生——例如這次測試中,刪除唯一的測試檔案後,workspace 裡就只剩 Nova 原本的 welcome/index.html),會直接判定為「唯一解」、自動註冊、recovered=True,全程沒有跳出候選清單給使用者確認的機會(那個分支只在候選數 > 1 時才會出現)。
影響
- 資料完整性/使用者困惑:使用者看到的 artifact 標題還是「QA-ARTIFACT02-heal-test」,但點開內容卻是別的東西(這次是無害的歡迎頁,但原則上可能是任何其他 artifact 的真實內容)
- 靜默、不可逆:整個過程沒有任何警告或確認 UI,使用者不會意識到自己的 artifact 已經被接到別人(或自己另一個)的檔案上
- 兩個 artifact 記錄共享同一個實體檔案後,其中一邊之後如果被編輯或(未來若支援)刪除底層檔案,會連帶影響另一個看似無關的 artifact
- severity 判定為中:不涉及跨使用者/跨帳號(同一個 agent 自己的 workspace 內),但資料完整性層面是實打實地把 A 的身份接到 B 的內容上
建議修復方向
_scan_workspace_for_kind() 或 heal_artifact() 的候選篩選階段,應該排除已經被其他 artifact_id 註冊為 entry file 的路徑(跟 ArtifactRepository 查一次即可)
- 或者,即使候選數剛好是 1,只要那個候選已被其他 artifact 佔用,就不要走「自動修復」快速路徑,改成跟「多候選」一樣的行為——列出來讓使用者確認,並附上警告「這個檔案已經是另一個 artifact(xxx)在用」
測試環境
https://dev-agent.narra.nexus
- 程式碼:
src/xyz_agent_context/artifact/_artifact_impl/heal.py(heal_artifact、_scan_workspace_for_kind)、backend/routes/agents/artifacts.py(heal 端點、delete_artifact 對照——後者有正確做到「只刪 DB row、不動檔案」)
- 對應測試計畫項目:
qa/features/collaboration-marketplace/test-plan-collaboration-marketplace.md TC-ARTIFACT-02
- 測試殘留已清理:透過 UI 刪除了
art_bb19e14e 的 registry 記錄(僅刪除 artifact 記錄本身,未影響 welcome/index.html 實體檔案,也未影響 Nova 原本的 art_5196fa1b)
摘要
Artifact 的「heal」自動修復流程(用來處理指標損壞的 pointer,見
POST /{agent_id}/artifacts/{artifact_id}/heal)在「workspace 裡剛好只掃到一個同副檔名候選檔案」時,會不經任何確認、靜默地把損壞的 artifact 重新註冊到那個候選檔案上——即使那個檔案已經是另一個 artifact 的 entry file。結果是兩個完全不同的 artifact 記錄(不同artifact_id、不同標題、不同建立時間)指向同一份實體檔案,其中一個的標題/身份跟它實際顯示的內容完全對不上。重現步驟(實測紀錄)
環境:
https://dev-agent.narra.nexus,xyzdev01,Nova(agent_4bf9d757910b)art_bb19e14e,標題「QA-ARTIFACT02-heal-test」,entry file 為qa_artifact02/index.htmlrm qa_artifact02/index.html),DB 裡的 artifact 記錄不動——模擬「pointer 損壞」情境GET /api/public/artifacts/raw/{token}/)→ 正確回 410{"detail":"artifact file missing on disk"},符合設計預期last-modified: 2026-06-18),完全不是這次測試建立的內容qa_artifact02/目錄確認為空(total 0),workspace 裡確認只剩這一個.html檔案。程式碼層根因
xyz_agent_context/artifact/_artifact_impl/heal.py的heal_artifact():_scan_workspace_for_kind()單純用副檔名掃 workspace 底下所有檔案,完全沒有排除「已經被其他 artifact_id 佔用當 entry file」的檔案。當掃描結果剛好只有 1 個候選(很容易發生——例如這次測試中,刪除唯一的測試檔案後,workspace 裡就只剩 Nova 原本的welcome/index.html),會直接判定為「唯一解」、自動註冊、recovered=True,全程沒有跳出候選清單給使用者確認的機會(那個分支只在候選數 > 1 時才會出現)。影響
建議修復方向
_scan_workspace_for_kind()或heal_artifact()的候選篩選階段,應該排除已經被其他 artifact_id 註冊為 entry file 的路徑(跟ArtifactRepository查一次即可)測試環境
https://dev-agent.narra.nexussrc/xyz_agent_context/artifact/_artifact_impl/heal.py(heal_artifact、_scan_workspace_for_kind)、backend/routes/agents/artifacts.py(heal端點、delete_artifact對照——後者有正確做到「只刪 DB row、不動檔案」)qa/features/collaboration-marketplace/test-plan-collaboration-marketplace.mdTC-ARTIFACT-02art_bb19e14e的 registry 記錄(僅刪除 artifact 記錄本身,未影響welcome/index.html實體檔案,也未影響 Nova 原本的art_5196fa1b)